Menu

TypeScript 타입 추론: 타입을 써야 할 때와 생략할 때

TypeScript는 작성한 값에서 대부분의 타입을 알아냅니다. 변수, let과 const, 객체, 배열, 반환값에 대해 무엇이 추론되는지, 콜백이 문맥에서 타입을 얻는 방식, 그리고 여전히 타입 표기가 필요한 곳을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

타입 추론은 작성하지 않은 타입을 TypeScript가 알아내는 것입니다. 변수를 초기화하면 값의 타입을 가지게 되고, 함수에서 값을 반환하면 반환 타입이 따라옵니다. 대부분의 TypeScript 코드는 생각보다 훨씬 적은 타입 표기만 가지고 있습니다.

주석은 에디터에서 각 이름 위에 마우스를 올렸을 때 보이는 타입입니다. 마지막 줄은 count에 타입을 쓴 적이 없는데도 컴파일 오류(TS2322)입니다. // @ts-expect-error가 이 오류를 예상된 것으로 표시하므로 블록의 나머지 코드는 실행됩니다.

let과 const: 리터럴 타입 확장(widening)

const는 절대 바뀌지 않으므로 TypeScript는 가장 좁은 타입, 즉 리터럴 값 자체를 타입으로 줍니다. let은 나중에 다시 대입될 수 있으므로 더 넓은 타입을 받습니다.

let a = "left";    // string
const b = "left";  // "left"
let c = 42;        // number
const d = 42;      // 42
const e = true;    // true

이 차이는 함수가 특정 값만 받을 때 중요해집니다.

index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.

const를 쓰거나 변수에 타입을 표기해서 고칩니다: let dir: "left" | "right" = "left". 리터럴 타입은 리터럴 타입 페이지에서 따로 다룹니다.

객체와 배열은 멤버의 타입을 확장한다

객체 리터럴의 프로퍼티는 변경할 수 있으므로, 객체 자체가 const여도 let처럼 타입이 확장됩니다. 배열 요소도 마찬가지입니다. as const를 쓰면 모든 것이 readonly가 되고 리터럴 타입이 유지됩니다.

반환 타입 추론

함수의 반환 타입은 return 문에서 추론됩니다. 여러 개라면 그 타입들의 유니언이 됩니다.

대부분의 함수에서는 추론된 반환 타입으로 충분합니다. 시그니처를 계약으로 삼고 싶은 export 함수나 공개 함수에는 반환 타입을 명시하세요. 그러면 본문을 수정해 다른 타입을 반환하게 됐을 때 멀리 떨어진 호출부가 아니라 함수 자체에서 오류가 납니다.

문맥적 타이핑(Contextual Typing): 콜백은 타입을 물려받는다

매개변수는 함수가 호출되는 방식에서 추론되지 않습니다. 예외가 두 가지 있습니다. 기본값이 있는 매개변수는 기본값의 타입을 가집니다(function retry(times = 3)이면 times는 number). 그리고 이미 함수 타입이 기대되는 자리에 작성한 함수는 그 문맥에서 매개변수 타입을 얻으므로, 콜백 매개변수에는 타입 표기가 필요 없습니다.

forEach, filter, reduce, Promise 콜백, 이벤트 핸들러, 그리고 함수 타입이 선언된 매개변수에 넘기는 모든 함수에 똑같이 적용됩니다.

매개변수에는 타입 표기가 필요하다

기본값도 없고 그런 문맥도 없다면, TypeScript는 함수가 어떻게 호출되는지를 보고 매개변수 타입을 짐작하지 않습니다. strict가 켜져 있으면 타입 표기가 없는 매개변수는 TS7006 오류입니다.

index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.

function double(x: number)로 쓰면 블록은 42를 출력하고, 반환 타입 number는 x * 2에서 추론됩니다.

최적 공통 타입(Best Common Type)

서로 다른 종류의 값이 섞인 배열 리터럴에 대해 TypeScript는 그 타입들의 유니언 배열을 추론합니다. 명시하지 않은 공통 상위 타입을 찾아 주지는 않습니다.

주의할 경우는 빈 배열 하나입니다. const list = []는 push할 때마다 타입이 늘어나는 "진화하는(evolving)" 배열로 시작합니다. const list: string[] = []처럼 타입을 표기하면 의도가 처음부터 드러나 더 명확합니다.

언제 타입을 표기할까

상황표기할까?
값으로 초기화한 변수아니요, 값이 타입을 정합니다
값 없이 선언한 변수(let result;)예, let result: string;
함수 매개변수예(문맥이 있는 콜백이나 기본값이 있는 경우 제외)
작은 내부 함수의 반환 타입선택
export 또는 공개 함수의 반환 타입계약으로서 권장
나중에 채울 빈 배열이나 객체예, const ids: number[] = []
값이 리터럴 타입을 유지해야 할 때as const 사용
값이 어떤 타입에 맞으면서 자신의 추론 타입을 유지해야 할 때satisfies 사용

무엇이 추론됐는지 가장 빨리 보는 방법은 에디터에서 이름 위에 마우스를 올리는 것입니다. 에디터가 없다면 const probe: boolean = mixed;처럼 틀린 타입의 변수에 값을 대입해 보세요. 컴파일러의 오류 메시지가 추론된 타입을 알려 줍니다.

자주 묻는 질문

TypeScript의 타입 추론이란 무엇인가요?

작성하지 않은 타입을 컴파일러가 알아내는 것입니다. let count = 0이면 count의 타입은 number가 되고, 두 숫자에 대해 a + b를 반환하는 함수는 반환 타입이 number가 됩니다. 추론된 타입은 직접 쓴 타입과 똑같이 검사됩니다.

TypeScript에서 모든 변수에 타입을 써야 하나요?

아니요. 함수 매개변수(호출에서 추론되지 않고, 기본값이나 콜백 문맥에서만 추론됩니다), 안정적인 API를 원할 때 export한 함수의 반환 타입, 값 없이 선언한 변수에 타입을 쓰세요. 나머지는 TypeScript가 추론하게 두면 됩니다. const name: string = "Ada"는 컴파일러가 이미 아는 것을 반복할 뿐입니다.

왜 const는 리터럴 타입으로, let은 string으로 추론되나요?

const는 다시 대입할 수 없으므로 const dir = "left"는 리터럴 타입 "left"가 됩니다. let은 나중에 다른 문자열을 담을 수 있으므로 let dir = "left"는 string으로 확장됩니다. 객체 리터럴의 프로퍼티도 다시 대입할 수 있어서 같은 확장이 일어나며, as const로 막을 수 있습니다.

매개변수가 암묵적으로 'any' 타입이라는 오류는 왜 나나요?

strict에 포함된 noImplicitAny가 내는 TS7006 오류입니다. TypeScript는 함수가 호출되는 방식에서 매개변수 타입을 추론하지 않으므로, 아무 표시도 없는 매개변수는 any가 됩니다. function double(x: number)처럼 타입을 쓰거나, map에 넘기는 콜백처럼 문맥에서 타입이 정해지는 곳에 함수를 넘기세요.

TypeScript가 추론한 타입은 어떻게 확인하나요?

에디터에서 이름 위에 마우스를 올리세요(VS Code를 비롯한 대부분의 에디터가 툴팁으로 추론된 타입을 보여 줍니다). 코드에서는 명백히 틀린 타입의 변수에 값을 대입하면 컴파일러가 오류 메시지에 추론된 타입을 출력합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기