TypeScript 5.5 — 실제 프로덕션 코드에 중요한 기능들
출처: Dev.to
TypeScript 5.5: 실제 프로덕션 코드에 중요한 기능들
TypeScript 5.5는 눈에 띄는 대형 기능과 미묘한 개선이 뒤섞여 대규모 코드베이스에서 크게 누적됩니다. 몇 달간 프로덕션에 적용해 본 결과, 우리 팀에 실제로 변화를 가져온 기능들을 정리했습니다.
자동 타입 프레디케이트 추론
작은 변화처럼 보이지만 개발자 경험(DX)이 크게 향상됩니다. 이제 TypeScript가 함수 구현부만 보고 타입 프레디케이트를 자동으로 추론합니다.
// 이전: TypeScript가 타입을 좁히지 못함
function isString(value: unknown): boolean {
return typeof value === 'string';
}
const values: (string | number)[] = ['hello', 42, 'world', 100];
// 이렇게 해야 했음:
const strings = values.filter((v) => {
if (isString(v)) {
return v; // TypeScript는 여전히 v가 number일 수도 있다고 생각함!
}
return false;
});
// 혹은 더 명시적이지만 장황한 방법:
const strings = values.filter((v): v is string => isString(v));
// 이제 TypeScript가 타입 프레디케이트를 자동으로 추론함
function isString(value: unknown): boolean {
return typeof value === 'string';
}
const values: (string | number)[] = ['hello', 42, 'world', 100];
// filter 콜백 내부에서 올바르게 타입이 좁혀짐
const strings = values.filter((v) => isString(v));
// strings는 정확히 string[] 타입
// 더 복잡한 프레디케이트도 동작
function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
const mixed: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];
const defined = mixed.filter(isNonNullable);
// defined는 string[] 타입
// 5.5 이전에는 모든 곳에 명시적인 타입 프레디케이트가 필요했음
interface User {
id: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
function isAdmin(user: User): boolean {
return user.role === 'admin';
}
function isActive(user: User): boolean {
return user.email.includes('@'); // 단순화된 예시
}
const users: User[] = /* API 로부터 받아옴 */;
// 기존 방식: 명시적인 타입 프레디케이트
const admins = users.filter((u): u is User => isAdmin(u) && isActive(u));
// 5.5 방식: 프레디케이트가 자연스럽게 추론됨
const admins = users.filter((u) => isAdmin(u) && isActive(u));
정규식 타입 검사
정규식에 대한 타입 검사가 추가되어 실제 버그를 잡아냅니다.
// 이전에는 런타임 에러가 발생했음
const emailRegex = new RegExp('[a-z+', 'i'); // 정규식 구문 오류!
// TypeScript 5.5: 컴파일 타임에 오류 감지
// Error: Invalid regular expression: Range out of order in character class
// 또 다른 예시
const dateRegex = new RegExp('(\\d{4})-(\\d{2})-(\\d{2}', 'g'); // 닫는 괄호 누락
// Error: Invalid regular expression: Unterminated group
// 실수로 잘못된 문자를 이스케이프한 경우
const phoneRegex = new RegExp('\\d{3}[-*]\\d{3}[-*]\\d{4}');
// 정상! 하지만 의도한 것이:
// const zipRegex = new RegExp('\d{5}'); // 백슬래시 누락!
// TypeScript 5.5가 잡아냄: 문자열 리터럴에서 '\d'는 '\\d'이어야 함
// Error: Invalid escape sequence in string literal
필터링 시 출력 타입 자동 추론
타입 프레디케이트로 배열을 필터링할 때, 이제 TypeScript가 출력 타입을 올바르게 추론합니다.
interface ApiResponse {
status: 'success' | 'error';
data?: object;
error?: string;
}
const responses: ApiResponse[] = await fetchAll();
// 5.5 이전: 필터 결과를 직접 타입 지정해야 함
const successful: ApiResponse[] = responses.filter((r) => r.status === 'success');
// 복잡한 상황에서는 번거로웠음
// 5.5에서는 차별화된 유니온을 더 잘 다룸
const successes = responses.filter((r) => r.status === 'success');
// successes는 (ApiResponse & { status: 'success' })[] 타입이며
// r.data는 이제 `object` 로 정확히 추론됨 (object | undefined 가 아님)
import assertions → import with 구문
// 5.5 이전: 이상한 edge case
import data from './data.json' assert { type: 'json' };
// 5.5 이후: 새로운 표준인 import attributes 구문
import data from './data.json' with { type: 'json' };
assert는 곧 with로 대체될 예정이며, 5.5는 미래에 대비한 코드를 작성하도록 돕습니다.
빌드 속도 개선
우리 코드베이스(~800k 라인, 300 패키지)에서 측정한 결과:
| 버전 | 전체 빌드 시간 | 증분 빌드 시간 |
|---|---|---|
| TypeScript 5.4 | 45.2 s | 12.1 s |
| TypeScript 5.5 | 38.7 s | 9.8 s |
- 전체 빌드: 약 14 % 개선
- 증분 빌드: 약 19 % 개선
속도 향상의 주요 원인:
- 더 효율적인 타입 좁히기
- 스마트한 stale 파일 감지
- 해결된 import 캐시 최적화
satisfies 동작 개선
// 5.5에서 union 타입에 대한 `satisfies` 의미가 정교해짐
type Color = 'red' | 'green' | 'blue';
type Theme = Record<string, string>;
const theme = {
primary: 'red',
secondary: 'blue',
custom: '#3498db', // 문자열이지만 Color 은 아님
} satisfies Theme;
// 이제 각 프로퍼티가 정확히 좁혀짐
const primary: Color = theme.primary; // 'red' (Color | string 아님)
const custom: string = theme.custom; // '#3498db' (Color | string 아님)
5.5 이전에는 satisfies만으로는 캐스팅이 필요했을 때가 있었습니다.
런타임 타입 지정은 여전히 필요
// 이 부분은 여전히 명시적인 타입 지정이 필요함 – TypeScript가 런타임을 추론할 수 없음
async function fetchUser(id: string): Promise<User> {
// User 타입을 알려줘야 함
const response = await fetch(`/api/users/${id}`);
return response.json(); // 여전히 any, 타입 어설션 또는 스키마 검증 필요
}
// 권장 방법: 스키마 검증기 사용 (zod, typebox 등)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
});
const response = await fetch(`/api/users/${id}`);
const user = UserSchema.parse(await response.json()); // 정확히 타입 지정됨
복잡한 추론은 아직 한계가 있음
function createReducer<S, A extends { type: string }>(
initialState: S,
handlers: Record<string, (state: S, action: A) => S>
): (state: S | undefined, action: A) => S {
return (state = initialState, action) => {
const handler = handlers[action.type];
return handler ? handler(state, action) : state;
};
}
// 일부 시나리오에서는 여전히 명시적인 타입이 필요함
// 이는 TypeScript 타입 시스템의 근본적인 특성
업그레이드 방법
npm install typescript@5.5 --save-dev
# 또는
pnpm add -D typescript@5.5
마이그레이션 체크리스트
-
filter 콜백에서
value is패턴 검색// 기존: 불필요하게 명시적 array.filter((item): item is Type => predicate(item)) // 이제: array.filter(predicate) -
import assertions 교체
// 기존 import foo from './foo.json' assert { type: 'json' }; // 교체 import foo from './foo.json' with { type: 'json' }; -
정규식 오류를 조기에 잡는 lint 규칙 추가
// ESLint 규칙: no-invalid-regexp -
CI에 커스텀 정규식 검증 스크립트 추가