본문으로 건너뛰기
← 블로그로 돌아가기
튜토리얼 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을 쓰는 것부터 시작하면 된다.