TypeScript 5.5 핵심 기능: 타입 가드 자동 추론 완전 정복
TypeScript 5.5 핵심 기능: 타입 가드 자동 추론 완전 정복
TypeScript 좀 써보신 분들이라면 이런 코드 지겹게 써보셨죠?
function isString(value: unknown): value is string { return typeof value === 'string'; }
저 value is string 있잖아요. 타입 프레디케이트라고 하는데요, 지금껏 타입 좁히는 함수 만들 때마다 저걸 일일이 써줘야 했어요.
TypeScript 5.5에서 이게 확 바뀌었어요.
Inferred Type Predicates라는 기능이 생겼는데요, TypeScript가 함수 코드를 보고 타입 프레디케이트를 알아서 만들어줘요. 이제 직접 안 써도 돼요. 깜빡하고 빼먹을 일도 없어요. 컴파일러가 다 알아서 해줘요.
그냥 편해진 정도가 아니에요. 타입 안전한 코드 짜는 방식 자체가 달라져요. 뭐가 바뀌었는지, 언제 되는지, 왜 좋은지 하나씩 살펴볼게요.
뭐가 문제였나: 타입 프레디케이트 일일이 쓰기 귀찮았음
TypeScript 5.5 전에는 타입 좁히기가 함수 안에서만 됐어요. 별도 함수로 빼는 순간 타입 정보가 날아갔거든요:
// 이건 됨 - 인라인으로 타입 좁히기 const values: (string | number)[] = ['a', 1, 'b', 2]; const strings = values.filter(value => typeof value === 'string'); // TypeScript 5.4: strings는 (string | number)[] ❌ // TypeScript 5.5: strings는 string[] ✅
5.4까지는요, filter에서 분명히 string만 남기는데도 TypeScript가 그걸 몰랐어요. 결과가 (string | number)[]로 나와서, 맞긴 한데 쓸모가 없었죠.
그래서 이렇게 타입 프레디케이트를 따로 써줘야 했어요:
function isString(value: string | number): value is string { return typeof value === 'string'; } const strings = values.filter(isString); // strings는 string[] ✅
간단한 예제에선 괜찮아 보이는데, 실제 프로젝트에선:
- 타입 프레디케이트 깜빡하고 왜 타입이 안 좁혀지지? 하고 삽질함
- 거짓말해도 에러 안 남 -
value is string써놓고 실제론 number 체크해도 통과됨 - 보일러플레이트 폭발 - 타입 가드가 수십 개면 다 써줘야 됨
해결책: TypeScript 5.5가 알아서 추론해줌
5.5부터는 컴파일러가 함수 코드를 분석해서 조건 맞으면 타입 프레디케이트를 자동으로 만들어줘요:
- 리턴 타입이나 타입 프레디케이트를 직접 안 썼을 때
- return문이 하나거나, 여러 개여도 같은 로직일 때
- 파라미터를 건드리지 않을 때
- 타입 좁히는 불리언을 리턴할 때
실제로 보면:
// TypeScript 5.5 - 그냥 이렇게만 써도 됨! function isString(value: unknown) { return typeof value === 'string'; } // TypeScript가 알아서 추론함: (value: unknown) => value is string const values: unknown[] = ['hello', 42, 'world', null]; const strings = values.filter(isString); // strings는 string[] ✅
컴파일러가 return문에서 typeof value === 'string' 보고, value is string 타입 프레디케이트를 자동으로 붙여줘요.
언제 추론이 되나?
TypeScript 5.5 추론은 마법이 아니에요. 규칙이 있어요. 이거 알면 새 기능 잘 활용할 수 있어요.
규칙 1: boolean을 리턴해야 함
타입 프레디케이트는 boolean 리턴하는 함수에만 의미 있어요:
// ✅ 추론됨 function isNumber(x: unknown) { return typeof x === 'number'; } // ❌ 안 됨 - 값을 리턴하니까 function getNumber(x: unknown) { return typeof x === 'number' ? x : null; }
규칙 2: 리턴 타입 직접 쓰면 안 됨
리턴 타입을 명시하면 TypeScript가 그걸 존중하고 추론 안 해요:
// ❌ 추론 안 됨 - boolean이라고 써버려서 function isString(value: unknown): boolean { return typeof value === 'string'; } // ✅ 추론됨 - 아무것도 안 썼으니까 function isString(value: unknown) { return typeof value === 'string'; }
필요하면 추론 끌 수 있게 일부러 이렇게 만든 거예요.
규칙 3: 파라미터 건드리면 안 됨
파라미터를 수정하면 추론이 꺼져요:
// ❌ 추론 안 됨 - 배열에 push함 function isNonEmptyArray(arr: unknown[]) { arr.push('something'); // 수정! return arr.length > 0; } // ✅ 추론됨 - 수정 없음 function isNonEmptyArray(arr: unknown[]) { return arr.length > 0; }
규칙 4: true가 진짜 타입 좁히는 의미여야 함
true 리턴할 때 그게 진짜로 타입을 좁히는 거여야 해요:
// ✅ 추론됨: true면 string function isString(x: unknown) { return typeof x === 'string'; } // ⚠️ 추론은 되는데 로직 잘 봐야 함 function isNotNull(x: string | null) { return x !== null; } // 추론: x is string (true일 때)
규칙 5: 배열 메서드가 진짜 꿀
.filter()가 이 기능의 핵심이에요. 필터 결과 타입을 제대로 좁혀줘요:
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2]; // 5.5 전: (string | number | null)[] // 5.5 후: (string | number)[] const nonNull = mixed.filter(x => x !== null); // 5.5 전: (string | number | null)[] // 5.5 후: string[] const strings = mixed.filter(x => typeof x === 'string'); // 체이닝도 됨! const upperStrings = mixed .filter(x => typeof x === 'string') .map(s => s.toUpperCase()); // s가 string으로 잘 추론됨
실전 예제: Before & After
예제 1: 옵셔널 프로퍼티 필터링
interface User { id: string; email?: string; phone?: string; } const users: User[] = [ { id: '1', email: '[email protected]' }, { id: '2', phone: '555-1234' }, { id: '3', email: '[email protected]', phone: '555-5678' }, ]; // 5.5 전: 헬퍼 함수 필요했음 function hasEmail(user: User): user is User & { email: string } { return user.email !== undefined; } // 5.5 후: 그냥 필터 쓰면 끝 const usersWithEmail = users.filter(u => u.email !== undefined); // 타입: (User & { email: string })[] usersWithEmail.forEach(u => { console.log(u.email.toUpperCase()); // 에러 없음! email이 string });
예제 2: 유니온 타입 분기
type Result<T> = | { success: true; data: T } | { success: false; error: string }; const results: Result<number>[] = [ { success: true, data: 42 }, { success: false, error: 'Failed' }, { success: true, data: 100 }, ]; // 5.5 전 function isSuccess<T>(result: Result<T>): result is { success: true; data: T } { return result.success; } const successResults = results.filter(isSuccess); // 5.5 후 - 자연스럽게 쓰면 됨 const successResults = results.filter(r => r.success); // 타입: { success: true; data: number }[] const sum = successResults.reduce((acc, r) => acc + r.data, 0); // 잘 됨!
예제 3: API 응답 검증
interface ApiResponse { status: number; data?: { items: string[]; }; } const responses: ApiResponse[] = await fetchMultipleEndpoints(); // 5.5 전: 프레디케이트 직접 써야 했음 function hasData(r: ApiResponse): r is ApiResponse & { data: { items: string[] } } { return r.status === 200 && r.data !== undefined; } // 5.5 후: 그냥 필터링 const validResponses = responses.filter( r => r.status === 200 && r.data !== undefined ); // data가 있는 타입으로 제대로 좁혀짐 const allItems = validResponses.flatMap(r => r.data.items); // ✅ 에러 없음
주의사항
주의 1: truthy 체크는 안 될 수도 있음
const values: (string | null | undefined)[] = ['a', null, 'b', undefined]; // ❌ 이건 안 됨 const truthy = values.filter(x => x); // 타입: (string | null | undefined)[] // ✅ 명확하게 체크해야 함 const defined = values.filter(x => x !== null && x !== undefined); // 타입: string[]
x가 truthy라고 타입이 확정되진 않아요. 빈 문자열도 falsy지만 string이니까요.
주의 2: Boolean 안 됨
const values: (string | null)[] = ['a', null, 'b']; // ❌ 안 됨 - Boolean은 그냥 함수로 취급 const filtered = values.filter(Boolean); // 타입: (string | null)[] // ✅ 화살표 함수로 const filtered = values.filter(x => x !== null); // 타입: string[]
Boolean이 타입 시스템에서 그냥 (value?: unknown) => boolean으로 정의되어 있어서요.
주의 3: 복잡한 조건은 안 될 수도
function isSpecialString(x: unknown) { if (typeof x !== 'string') return false; if (x.length < 5) return false; if (!x.startsWith('prefix')) return false; return true; } // 이런 복잡한 건 추론 안 될 수도 있음
복잡한 검증 로직은 여전히 타입 프레디케이트 직접 쓰는 게 나아요.
성능 걱정?
컴파일 시간 늘어나는지 궁금하실 텐데요.
거의 안 늘어요. TypeScript가 이미 함수 안에서 타입 좁히기 위해 코드 분석하고 있거든요. 리턴 타입 추론은 그 분석 결과를 재활용하는 거예요.
실제 테스트에서 컴파일 시간 차이는 측정 오차 수준이에요.
업그레이드 가이드
1단계: TypeScript 업데이트
npm install [email protected] --save-dev # 또는 yarn add [email protected] --dev # 또는 pnpm add [email protected] --save-dev
2단계: 불필요한 타입 프레디케이트 정리
이제 TypeScript가 추론하는 건 지워도 돼요:
// 전: 직접 써줌 function isNumber(x: unknown): x is number { return typeof x === 'number'; } // 후: 알아서 추론되게 function isNumber(x: unknown) { return typeof x === 'number'; }
단, 복잡한 로직이나 공개 API는 남겨두세요. 문서화 목적으로도 유용하니까요.
3단계: filter() 사용처 점검
여기서 제일 큰 효과 볼 수 있어요:
- 인라인 프레디케이트 쓰는
filter()호출 - 필터링용 헬퍼 함수들
많이 단순화할 수 있어요.
마무리
TypeScript 5.5의 Inferred Type Predicates는 진짜 꿀 기능이에요:
- 보일러플레이트 확 줄어듦 -
value is Type매번 안 써도 됨 - 실수할 일 없음 - 컴파일러가 만드니까 거짓말 못 함
- filter() 드디어 제대로 동작 - 필터 결과 타입이 기대대로 나옴
업그레이드 쉽고, 성능 영향 없고, 효과는 바로 나타나요. 아직 5.5 아니면 이거 하나만으로도 올릴 가치 있어요.
이제 value is string 백 번째 쓸 일 없어요. TypeScript가 알아서 해줘요.