[220322_TIL] typescript를 배워보자 | Everyday Types

40534 단어 typescriptTILTIL

전체 코드는 git 에서 확인 가능합니다. 작성된 내용은 ts 공식문서를 학습하며 주관적으로 재구성한 내용으로, 오류가 있을 수 있습니다. 잘못된 내용은 댓글로 알려주시면 감사히 수정하겠습니다 : )

변수 타입

ts에서의 타입은 (당연하게도) js의 타입과 많이 닮아있다. number, string, boolean 은 타입의 선언 방식 또한 동일하다. 또한 const, var, 또는 let 등을 사용하여 변수를 선언할 때, 변수의 타입을 명시적으로 지정하기 위하여 타입 표기를 추가할 수 있으며 이는 선택 사항이다.

배열은 두가지 방법으로 타입을 설정할 수 있다. Array<number>number[]는 완전히 동일하다.

// 1.1 변수에 대한 타입 표기 - 원시타입(선택사항)
const name: string = "jisu"
const age: number = 10
const toggle: boolean = true

// 1.2 Array
const numArray1: Array<number> = [1, 2, 3]
const numArray2: number[] = [4, 5, 6]
const strArray1: Array<string> = ["a", "b", "c"]
const strArray2: string[] = ["e", "f", 'g']

// 1.3 any
let obj: any = {x: 0}
obj.foo() // any 지정해서 오류 안 생김
obj()
obj.bar = 100
obj = "hello"
const n: number = obj;

주의할 점으로, any 를 사용하면, ts는 개발자가 알아서 오류없이 작성할 것이라고 믿고 타입 검사를 하지 않는다. 그래서 런타임에서 오류가 난다. 되도록이면 any 대신 정확한 타입을 써야한다.

함수 타입

함수에서는 정의할 타입은 크게 input, ouput 이 있다. 즉, 파라미터와 반환값의 타입을 지정할 수 있다.

function addFive(x: number): number {
    return x + 5
}

위와 같이 파라미터와 반환값에 대한 타입을 명시할 수 있다. 이렇게 작성하고 나면 함수를 호출할 때 명시한 타입과 다르면 경고메시지를 보여준다. 참으로 친절하다!

익명 함수

forEach 의 파라미터로 들어가는 익명함수에 대해서, 파라미터 s 에 대한 타입을 지정하지 않아도 string 이라는 것을 알아채고, 경고메시지를 준다.

const names = ["Alice", "Bob", "Eve"];
names.forEach((s) => {
    console.log(s.toUppercase()) // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
})

이는 ts가 문맥적 타입 부여를 한 것으로, 타입을 지정하지 않아도 ts가 알아서 판단한 것이다.

객체 타입

js에서 가장 많이 사용되고 중요한 객체 타입이다.

function printCoord(pt: { x: number; y: number }) {
    console.log("The coordinate's x value is " + pt.x);
    console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

point를 의미하는 pt 객체가 있고, pt의 속성인 x, y가 number 인지 확인한다. 프로퍼티 사이에는 ;를 사용해도 되고 , , 콤마를 사용해도 된다. 만약 타입을 지정하지 않으면, any로 간주된다.

옵셔널 프로퍼티

아래 함수는 인자를 선택적으로 받는 경우에 사용할 수 있다. 객체의 프로퍼티 뒤에 ?를 붙여 선택적 프로퍼티라는 것을 의미한다. 있어도 되고, 없어도 문제 없는 것이다. 다만, 선택 사항이기 때문에, 없는 경우에도 문제가 되지 않게 분기처리해야 한다.

function printName(obj: { first: string; last?: string }) {
    console.log(obj.last.toUpperCase()); // 오류 - `obj.last`의 값이 제공되지 않는다면 프로그램이 멈추게 됩니다!

    if (obj.last !== undefined) {
      // OK
      console.log(obj.last.toUpperCase());
    }
   
    // 최신 JavaScript 문법을 사용하였을 때 또 다른 안전한 코드
    console.log(obj.last?.toUpperCase());
}
printName({ first: "Bob" }); // OK
printName({ first: "Alice", last: "Alisson" }); // OK

유니언 타입

유니언은 합집합, or 을 의미한다. 두가지 이상의 타입을 허용하는 방법이다.

다음의 함수는 id라는 파라미터를 받는데, 이때 id는 number이거나 string 이다.
그래서 위 두 타입이 아닌, 다른 타입을 입력했을 때는 오류가 발생한다.

function printId(id: number | string) {
    console.log("Your ID is: " + id); // num, str 둘다 오류 없는 작업
    console.log(id.toUpperCase()); // num 일때 오류 발생
}
printId(101); // OK
printId("202"); // OK
printId({ myID: 22342 }); // obj 여서 오류

그리고 둘 이상의 타입을 받을 때는, 함수 안에서 공통적으로 작동할 수 없는 동작에 대해서는 분기처리를 할 필요가 있다.

유니언 좁히기

위에서 number, string을 둘을 허용하고, 함수 내부적으로 타입 검사를 통해 서로 다른 결과값을 출력할 수 있다. 이때 파라미터 타입이 정확히 number 또는 string 이므로, 조건문은 if-else 면 충분하다.

function printId2(id: number | string) {
    if (typeof id === "string") {
        // 이 분기에서 id는 'string' 타입을 가집니다

        console.log(id.toUpperCase());
    } else {
        // 여기에서 id는 'number' 타입을 가집니다
        console.log(id);
    }
}

또 다른 방법으로, 배열을 받는 경우 isArray() 등으로 확인 할 수도 있다.

function welcomePeople(x: string[] | string) {
    if (Array.isArray(x)) {
      // 여기에서 'x'는 'string[]' 타입입니다
      console.log("Hello, " + x.join(" and "));
    } else {
      // 여기에서 'x'는 'string' 타입입니다
      console.log("Welcome lone traveler " + x);
    }
}

공통적 특성을 가지는 유니언

다음 함수는 첫 3개를 반환하는 함수로, 배열 또는 문자열을 입력 받는다. 둘다 이터러블한 객체이기때문에 slice() 메서드를 가지므로, 분기할 필요가 없다. 반환타입은 number[] | string이다.

function getFirstThree(x: number[] | string) {
    return x.slice(0, 3);
}

타입 별칭

여러 함수에 동일한 파라미터가 사용되는 경우, 중복을 최소화 하기 위해 타입별칭을 사용할 수 있다.

type Point = {
    x: number;
    y: number;
};
   
function printCoord2(pt: Point) { // 위의 타입으로 간편하게 사용
    console.log("The coordinate's x value is " + pt.x);
    console.log("The coordinate's y value is " + pt.y);
}
   
printCoord2({ x: 100, y: 100 });

자주 사용되는 객체 타입을 타입 별칭으로 선언하여 사용 할 수 있다.

유니온 타입 별칭

타입 별칭을 그저 자주 사용하는 타입을 사용하기 좋게 선언할 뿐이다. 그래서 다음과 같이 유니언도 따로 이름을 부여할 수 있다.

type ID = number | string; 

인터페이스

인터페이스는 타입과 비슷한 듯 다른데, 가장 큰 차이점인 인터페이스는 object의 타입만 정의한다는 것이다.

위에서 타입 별칭으로 작성된 것을 interface로 동일하게 작성할 수 있다.

interface MyPoint {
    x: number;
    y: number;
}
   
function printCoord3(pt: MyPoint) { // 파라미터의 구조에만 관심, 타입 검사
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}

printCoord3({ x: 100, y: 100 });

인터페이스 vs 타입 별칭

ts 공식 문서에서 설명하는 차이점 예시이다.

많은 경우 타입 별칭보다는 인터페이스를 많이 사용하는 것 같다.

타입 단언

ts가 제안하는 타입보다 더 정확하고 자세한 타입을 명시하고 싶을 때 사용한다.

둘 다 동일한 방법으로, as를 사용하거나 꺽쇠로 앞에 명시할 수 있다. 개발자가 element의 id를 정확하게 입력했고, 그러한 객체가 canvas하는 것을 정확히 알고 있을 때 사용하는 것이다. 기본적으로 document.getElementById의 타입은 HTMLElement 이다.

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

리터럴 타입

문자와 숫자의 경우 오직 몇개의 값만 허용하고 싶은 경우가 있다. 이때 일반적으로 변수를 선언할 때를 생각해보면 const 를 사용하면, 값을 수정할 수 없는데 이를 활용한 방법이다.

let changingString = "Hello World";
changingString = "Olá Mundo";
// 변수 `changingString`은 어떤 문자열이든 모두 나타낼 수 있으며,
// 이는 TypeScript의 타입 시스템에서 문자열 타입 변수를 다루는 방식과 동일합니다.
changingString;
      
let changingString: string
 
const constantString = "Hello World";
// 변수 `constantString`은 오직 단 한 종류의 문자열만 나타낼 수 있으며,
// 이는 리터럴 타입의 표현 방식입니다.
constantString;

변수를 선언할 때 let 을 사용하면 해당 변수의 타입은 그의 상위 타입, 즉 위 경우에는 string으로 추론된다. 반면 const 를 사용하면, 선언했던 그 값으로 정확히 타입이 지정된다.

let을 사용하고 리터럴 타입을 지정하고 싶을 때는 다음과 같이 작성할 수 있다.

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";

함수에서의 사용

리터럴 타입은 함수등에서 사용될 때 진가가 발휘된다. 다음과 같이 미리 지정된 몇개의 옵션만 적용하기 위해 리터럴 타입을 사용할 수 있고, 유니온으로 작성한 목록들 외에 다른 값이 들어오면 경고를 보여준다.

function printText(s: string, alignment: "left" | "right" | "center") {
    // ...
  }
printText("Hello, world", "left");
printText("G'day, mate", "centre"); // 오타 감지

파라미터 뿐만 아니라 함수의 반환값에서도 사용 가능하다.

function compare(a: string, b: string): -1 | 0 | 1 {
    return a === b ? 0 : a > b ? 1 : -1;
}

리터럴 타입의 유니온이 아닌, 다른 type 과 같이 사용하는 것도 가능하다.

interface Options {
    width: number;
  }

function configure(x: Options | "auto") {
    // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");

리터럴 추론

js에서 객체는 const로 선언하여도, 프로퍼티를 수정하거나 추가 할 수 있다. 그렇기 때문에 let 처럼 동작하며, 리터럴 추론이 원하는대로 동작하지 않을 수 있다.

declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // "GET" 이라는 리터럴이 아닌, 문자열로 추론되어 오류 발생

handleRequest 함수는 urlmethod 두 개의 파라미터를 입력 받는다. 이때 method는 리터럴 타입으로 오직 GET과 POST만 입력받고자 한다.

그런데 req 객체는 각 프로퍼티가 위 함수의 파라미터와 정확히 일치하는 것 같지만, 위에서 말한 것 처럼 오브젝트의 속성을 수정가능한 let 처럼 간주되어 리터럴이 아닌 문자열로 추론되어 오류가 발생한다.

이를 해결하기 위해 몇가지 방법이 존재한다.

해결 방법1. 타입 단언 추가

as 를 사용하여 보다 자세하고 정확한 타입을 명시하는 방법이다.

// 수정 1:
const req2 = { url: "https://example.com", method: "GET" as "GET" };
// 수정 2
handleRequest(req2.url, req2.method as "GET");

해결 방법2. as const로 객체 전체를 리터럴로 변경

객체를 수정 불가한 const로 변환하는 것이다.

const req3 = { url: "https://example.com", method: "GET" } as const;
handleRequest(req3.url, req3.method);

nullundefined

strictNullChecks 옵션을 설정하지 않으면, 타입을 지정하더라도 null, undefied가 허용된다. 따라서 보다 엄격한 타입 지정을 위해 위 옵션을 켜고 사용하는 것이 좋다.

strictNullChecks 를 설정한 경우 유니온 타입을 사용한 후 조건 분기를 통해 오류를 방지할 수 있다.

function doSomething(x: string | undefined) {
    if (x === undefined) {
      // 아무 것도 하지 않는다
    } else {
      console.log("Hello, " + x.toUpperCase());
    }
}

또한 null 이 아니라는 것이 명확한 경우에는 null 이 아님을 단언하는 !를 사용하여 다음과 같이 작성할 수 도 있다.

function liveDangerously(x?: number | undefined) {
    // 오류 없음
    console.log(x!.toFixed());
}

Reference

좋은 웹페이지 즐겨찾기