타입 추론과 제어 흐름 분석
- 3.1 추론 가능한 타입을 사용해 장황한 코드 방지하기
- 3.2 다른 타입에는 다른 변수 사용하기
- 3.3 타입 좁히기
- 3.4 타입의 진화 이해하기
- 3.5 함수형 기법과 라이브러리로 타입 흐름 유지하기
- 3.6 비동기 코드에는 콜백 대신 async 함수 사용하기
- 3.7 커링과 새로운 추론 영역 이해하기
TS가 알아서 타입을 알아내는 방식을 이해하면, 코드가 훨씬 깔끔해져. 모든 변수에 타입을 직접 다는 건 사실 TS 초보가 가장 많이 하는 실수 중 하나거든.
// 이렇게 하지 마
const x: number = 12;
const person: { name: string; age: number } = { name: 'Alice', age: 30 };
// 이렇게 해 — TS가 추론한다
const x = 12;
const person = { name: 'Alice', age: 30 };
TS의 타입 추론은 매우 강력해서, 변수 초기화 값으로부터 타입을 정확히 알아내. 불필요한 타입 명시는 코드를 장황하게 만들고, 값을 바꿀 때 타입도 같이 바꿔야 하는 번거로움을 만들 뿐이야. 그렇다면 언제 타입을 명시해야 하냐면 — 함수의 반환 타입(구현 오류를 더 빨리 잡을 수 있고 문서 역할도 하니까), 객체 리터럴(잉여 속성 체크를 받고 싶을 때), 그리고 추론이 문맥에서 벗어날 때야. 핵심은 타입 추론이 맞게 해주면 명시하지 말고, 가독성을 높이거나 오류를 잡아주는 경우에만 쓰라는 거지.
JS에서는 하나의 변수에 여러 타입의 값을 넣는 게 자연스러운데, TS에서는 이게 에러야. 유니온 타입(string | number)으로 선언하면 되긴 하지만, 저자는 다른 타입에는 다른 변수를 쓰라고 조언해. 변수의 역할이 명확해지고, 타입이 좁아지니까 자동완성이 더 잘 되고, const를 쓸 수 있어서 불변성도 확보되거든. 변수 하나에 유니온 타입을 넣으면 사용할 때마다 타입을 좁혀야 하는 번거로움이 생기잖아.
**타입 좁히기(type narrowing)**는 TS가 제어 흐름을 분석해서 넓은 타입을 좁은 타입으로 좁히는 메커니즘이야. 3장에서 가장 실전적인 내용이지.
function process(value: string | number) {
if (typeof value === 'string') {
value.toUpperCase(); // 여기서 value는 string
} else {
value.toFixed(2); // 여기서 value는 number
}
}
typeof 체크, instanceof 체크, 속성 체크(in), 그리고 가장 강력한 태그드 유니온(판별 유니온) 패턴까지 다양한 좁히기 기법이 있어.
interface Circle { kind: 'circle'; radius: number; }
interface Square { kind: 'square'; side: number; }
type Shape = Circle | Square;
function area(shape: Shape) {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2; // shape은 Circle
} else {
return shape.side ** 2; // shape은 Square
}
}
태그드 유니온은 TS에서 가장 자주 쓰이는 패턴 중 하나야. kind 같은 리터럴 타입 속성이 태그 역할을 해서 안전하게 분기할 수 있게 해주거든. TS의 내장 좁히기로 부족할 때는 사용자 정의 타입 가드를 쓸 수도 있어. 반환 타입이 el is HTMLInputElement 같은 **타입 서술어(type predicate)**인 함수인데, 강력하지만 위험하기도 해 — 타입 가드의 구현이 틀려도 TS가 검증하지 않거든. 가드 함수가 true를 반환하면 TS는 무조건 믿으니까, 구현을 잘 해야 해.
변수의 타입은 선언 시점에 결정되는 게 보통이지만, 특정 상황에서는 **타입이 진화(evolve)**하기도 해. any[]로 시작한 배열이 값을 넣을수록 number[], (string | number)[]로 넓어지는 거야. 저자는 이 동작을 이해는 하되 의존하지 말라고 해. 명시적으로 타입을 지정하는 게 더 안전하고 가독성도 좋으니까.
명령형 코드(for 루프)보다 함수형 기법(map, filter, reduce)을 쓰면 타입 흐름이 끊기지 않아.
// 함수형 — 타입이 자연스럽게 흐른다
const names = people.map(p => p.name); // string[]
함수형 기법을 쓰면 TS가 각 단계의 타입을 자동으로 추론하기 때문에 중간 변수에 타입을 명시할 필요가 없지. Lodash 같은 라이브러리도 잘 타이핑되어 있어서 체이닝 패턴에서 타입이 자연스럽게 유지돼.
비동기 코드에서도 마찬가지야. **async/await**를 쓰면 비동기 코드가 동기 코드처럼 읽히고, 타입 추론이 자연스럽게 동작하고, try/catch로 에러 처리가 일관적이야. 저자가 특히 강조하는 건 — Promise를 반환하는 함수는 항상 async로 만들라는 거야. async가 아닌 함수에서 Promise를 반환하면 동기 예외와 비동기 거부가 섞여서 에러 처리가 일관성이 없어지거든.
2판에서 새로 추가된 내용으로, 함수를 **커링(currying)**하면 새로운 추론 영역이 생긴다는 게 있어. 하나의 함수 호출에서 여러 제네릭 타입을 한꺼번에 추론하는 것보다, 커링으로 나눠서 순차적으로 추론하는 게 TS가 더 잘 해내거든. 복잡한 제네릭 함수에서 타입 추론이 실패할 때 커링으로 분리하면 해결되는 경우가 있어. 실전에서는 빌더 패턴이나 함수형 프로그래밍 라이브러리에서 이 패턴을 많이 볼 수 있지.
정리
3장 읽고 기억할 거 세 가지:
- TS의 타입 추론을 믿어라. 추론이 정확한 곳에 타입을 명시하는 건 불필요한 노이즈다. 반환 타입 등 전략적인 곳에만 명시하라.
- 태그드 유니온이 타입 좁히기의 최강 패턴이다. 판별 속성 하나로 안전하게 분기할 수 있다.
- 함수형 기법과 async/await로 타입 흐름을 유지하라. 명령형 루프와 콜백은 타입 추론을 끊는다.