← 블로그로 돌아가기
튜토리얼 2025년 11월 5일
TypeScript 실전 팁 — 타입 좁히기, 제네릭, 유틸리티 타입
실무에서 자주 쓰는 TypeScript 패턴. 타입 좁히기, 제네릭 활용, 유틸리티 타입 정리.
TypeScript 타입 프론트엔드 개발 팁
TypeScript를 쓰면서 달라지는 것
TypeScript의 가치는 에디터 자동 완성과 컴파일 타임 에러 검출이다. 런타임에 터질 버그를 코드 작성 시점에 잡아준다. 하지만 타입을 any로 도배하면 JavaScript와 다를 게 없다. 타입을 제대로 활용하는 패턴을 정리한다.
1. 타입 좁히기 (Type Narrowing)
유니온 타입에서 특정 타입으로 좁히는 패턴이다.
typeof 가드
function formatValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase(); // string 메서드 사용 가능
}
return value.toFixed(2); // number 메서드 사용 가능
}
in 연산자
interface BlogPost {
title: string;
content: string;
}
interface PortfolioItem {
title: string;
tags: string[];
}
function getDescription(item: BlogPost | PortfolioItem): string {
if ('content' in item) {
return item.content.slice(0, 100); // BlogPost
}
return item.tags.join(', '); // PortfolioItem
}
판별 유니온 (Discriminated Union)
가장 강력한 패턴이다. type 필드로 구분한다.
type ApiResponse =
| { status: 'success'; data: unknown }
| { status: 'error'; message: string }
| { status: 'loading' };
function handleResponse(res: ApiResponse) {
switch (res.status) {
case 'success':
console.log(res.data); // data 접근 가능
break;
case 'error':
console.error(res.message); // message 접근 가능
break;
case 'loading':
// data, message 모두 접근 불가 (정확함)
break;
}
}
switch의 각 case에서 TypeScript가 자동으로 타입을 좁혀준다.
2. 제네릭 (Generics)
“아직 정해지지 않은 타입”을 파라미터로 받는 패턴이다.
기본 사용
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // number | undefined
const str = first(['a', 'b', 'c']); // string | undefined
제약 조건 (Constraints)
제네릭에 최소 조건을 붙인다.
function getTitle<T extends { title: string }>(item: T): string {
return item.title;
}
// BlogPost, PortfolioItem 모두 사용 가능 (둘 다 title이 있으므로)
getTitle({ title: 'Hello', content: '...' }); // OK
getTitle({ name: 'World' }); // 에러: title 없음
키 제약 (keyof)
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const post = { title: 'Hello', date: '2026-01-01', tags: ['ts'] };
const title = pick(post, 'title'); // string
const tags = pick(post, 'tags'); // string[]
// pick(post, 'author'); // 에러: 'author'는 keyof Post가 아님
3. 유틸리티 타입
TypeScript 내장 유틸리티로 기존 타입을 변환한다.
Partial — 모든 필드를 선택적으로
interface User {
name: string;
email: string;
age: number;
}
// 업데이트 시 일부 필드만 전달
function updateUser(id: string, updates: Partial<User>) {
// updates.name은 string | undefined
}
updateUser('123', { name: 'New Name' }); // OK, email과 age 생략 가능
Pick / Omit — 필드 선택/제외
// 목록에서는 일부 필드만 사용
type UserListItem = Pick<User, 'name' | 'email'>;
// 생성 시에는 id를 제외
type CreateUser = Omit<User & { id: string }, 'id'>;
Record — 키-값 매핑
type CategoryLabel = Record<string, string>;
const labels: CategoryLabel = {
dev: '개발',
design: '디자인',
insight: '인사이트',
};
ReturnType — 함수 반환 타입 추출
function fetchUser() {
return { name: 'John', email: 'john@example.com' };
}
type User = ReturnType<typeof fetchUser>;
// { name: string; email: string }
4. as const
리터럴 타입을 유지한다.
// as const 없이
const colors = ['red', 'blue', 'green']; // string[]
// as const
const colors = ['red', 'blue', 'green'] as const; // readonly ['red', 'blue', 'green']
type Color = (typeof colors)[number]; // 'red' | 'blue' | 'green'
설정 객체나 상수 배열에서 유용하다. 타입을 별도로 정의하지 않아도 리터럴 유니온이 자동으로 생성된다.
5. 실수하기 쉬운 것들
any 대신 unknown
// 나쁜 예
function parse(json: string): any {
return JSON.parse(json);
}
// 좋은 예
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{}');
// data.name; // 에러: unknown에는 접근 불가
// 타입 좁히기 필요
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name); // OK
}
unknown은 안전한 any다. 타입 좁히기를 강제한다.
옵셔널 체이닝과 nullish coalescing
const name = user?.profile?.displayName ?? 'Anonymous';
?.는 중간에 null/undefined면 undefined를 반환하고, ??는 null/undefined일 때만 대체값을 사용한다. ||와 달리 빈 문자열이나 0을 유효한 값으로 취급한다.
정리
TypeScript의 힘은 타입을 정밀하게 다룰 때 나온다. 판별 유니온으로 상태를 모델링하고, 제네릭으로 재사용 가능한 함수를 만들고, 유틸리티 타입으로 기존 타입을 변환하자. any를 줄이고 unknown을 쓰는 것부터 시작하면 된다.