타입 인터페이스

TypeScript 타입 추론

TypeScript는 간단한 규칙 몇 가지를 기반으로 변수의 타입을 추론하고 검사할 수 있습니다. 이 규칙들은 꽤 간단하기 때문에 여러분들의 뇌를 안전한 코드와 그렇지 않은 코드를 구별할 수 있게 훈련시킬 수 있을 것입니다. (이런 현상은 저에게도 일어났고, 제 팀원들에게도 꽤나 빨리 일어났습니다.)

앞으로 이야기할 "타입 흐름"(=types flowing)이라는 것은 타입에 관한 정보의 흐름 즉, 타입을 추론하는 논리적 사고 흐름을 뜻합니다.

변수 정의

변수의 타입은 변수의 정의로부터 추론됩니다.

let foo = 123; // foo는 `number`입니다
let bar = "Hello"; // bar는 `string`입니다
foo = bar; // Error: `string`을 `number`에 할당할 수 없습니다.

위 예제는 타입 흐름이 오른쪽에서 왼쪽으로 흐르는 경우입니다.

함수 리턴 타입

리턴 타입은 함수 반환문에 의해 추론됩니다. 가령 아래의 함수는 number를 리턴하는 것으로 추론됩니다.

function add(a: number, b: number) {
    return a + b;
}

위 예제는 타입 흐름이 아래에서 밖으로 흐르는 경우입니다.

할당 (=Assignment)

함수 매개 변수와 리턴 값의 타입은 할당을 통해서도 추론이 가능합니다. 아래의 코드를 보시면 fooAdder 타입이라고 정의했기 때문에 ab의 타입은 자연스레 number가 되는 것이죠.

type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => a + b;

이는 아래의 코드를 통해서도 확인 가능한데요, 다들 예상하셨듯이 아래 코드는 에러를 발생시킬 것입니다:

type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => {
    a = "hello"; // Error: `string`을 `number`에 할당할 수 없습니다.
    return a + b;
}

위 예제는 타입 흐름이 왼쪽에서 오른쪽으로 흐르는 경우였습니다.

이와 같은 할당 방식의 타입 추론법은 callback 함수에게도 동일하게 적용됩니다. 즉, 결국 모든 인수(argument) → 매개 변수(parameter)로 가는 구조는 또 다른 형태의 변수 할당일 뿐입니다.

type Adder = (a: number, b: number) => number;
function iTakeAnAdder(adder: Adder) {
    return adder(1, 2);
}
iTakeAnAdder((a, b) => {
    // a = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.
    return a + b;
})

구조화 (=Structuring)

이 간단한 규칙들은 구조화(=객체 리터럴 생성)의 경우에도 동일하게 적용됩니다. 예를 들어 다음 코드의 경우 foo의 타입은 {a: number, b: number}로 추론됩니다.

let foo = {
    a: 123,
    b: 456
};
// foo.a = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.

배열도 마찬가지인데요:

const bar = [1,2,3];
// bar[0] = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.

그리고 어떠한 형태의 중첩 구조에서도 동일한 규칙이 적용됩니다:

let foo = {
    bar: [1, 3, 4]
};
// foo.bar[0] = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.

비구조화 (=Destructuring)

비구조화 할당에도 앞서 살펴본 규칙은 동일하게 적용됩니다. 객체와:

let foo = {
    a: 123,
    b: 456
};
let {a} = foo;
// a = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.

배열 모두에게 말이죠:

const bar = [1, 2];
let [a, b] = bar;
// a = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.

그리고 함수 매개 변수의 타입이 추론 가능하다면, 비구조화 할당된 property의 타입도 당연히 추론이 가능하겠죠? 아래의 예시는 함수의 인자를 a, b 멤버로 비구조화 할당하고 있습니다.

type Adder = (numbers: { a: number, b: number }) => number;
function iTakeAnAdder(adder: Adder) {
    return adder({ a: 1, b: 2 });
}
iTakeAnAdder(({a, b}) => { // `a`와 `b`의 타입이 추론됩니다.
    // a = "hello"; // ERROR: `string`을 `number`에 할당할 수 없습니다.
    return a + b;
})

타입 가드 (=Type Guards)

우리는 앞서 타입 가드가 어떤 방식으로 타입을 변경하고 타입의 범주를 좁혀 나가는지를 살펴봤습니다. (특히 union의 경우에서 자세히 살펴보았죠.) 사실 타입 가드는 그저 또 다른 방식으로 블록 내 변수의 타입을 추론하는 형태일 뿐입니다.

주의사항

매개 변수를 주의하세요

함수 매개 변수의 타입은 할당으로 타입을 추론할 수 없는 경우에는 타입 정보가 전달되지 않을 것입니다(=타입 추론을 할 수 없습니다). 가령 아래의 경우 컴파일러가 foo의 타입을 모르기 때문에, ab의 타입 또한 추론할 수 없습니다.

const foo = (a, b) => { /* do something */ };

그러나 만약 foo의 타입이 정의되었다면, 함수 매개 변수의 타입 또한 추론이 가능했을 겁니다. (아래 예시를 보면 ab가 모두 number로 추론되는 것을 확인할 수 있습니다.)

type TwoNumberFunction = (a: number, b: number) => void;
const foo: TwoNumberFunction = (a, b) => { /* do something */ };

리턴을 주의하세요

일반적으로 TypeScript는 함수의 리턴 타입을 추론하는 편이긴 하지만 종종 여러분이 예상한 것과 다른 결과가 나올 수도 있습니다. 가령 아래의 함수 fooany를 리턴 타입으로 가지게 될 것입니다.

function foo(a: number, b: number) {
    return a + addOne(b);
}
// JavaScript로 만든 라이브러리의 외부 함수 addOne
function addOne(a) {
    return a + 1;
}

이와 같은 현상은 리턴 타입이 느슨하게 정의가 된 addOne 때문에 발생하는데요, aany이기 때문에 addOne의 리턴 타입도 any가 되고, 이는 foo에도 동일하게 적용되기 때문입니다.

저는 함수의 리턴 타입은 항상 명시적으로 선언하는 것이 가장 심플한 방법이라는 결론을 내렸습니다. 어쨌든 결국 타입 주석은 가설이고, 함수문이 증명일 테니까요.

이 외에도 더 많은 경우의 수를 생각해볼 수 있겠지만 여기서 한 가지 좋은 소식을 먼저 알려드리자면, 이러한 종류의 버그를 쉽게 잡을 수 있도록 도와주는 컴파일러 플래그가 이미 존재한다는 것입니다.

noImplicitAny

noImplicitAny는 컴파일 도중 타입 추론이 불가능해 암묵적으로 any 타입을 가지는 변수를 발견 시 에러를 발생시키도록 지시하는 플래그입니다. 이 에러를 핸들 하려면:

  • : any 타입 주석을 추가해 응~ 그래 나는 이 친구가 any 타입이길 원해~라고 명시적으로 선언하거나

  • 컴파일러가 타입을 잘 추론할 수 있게 좀 더 정확한 주석을 추가하는 방법이 있습니다.

Last updated