TypeScript 치트시트

TypeScript 핵심 타입 시스템과 고급 패턴 모음 — 기본 타입, 제네릭, 유틸리티 타입, 타입 가드, 고급 타입, 실전 패턴

📊

기본 타입

6 items

Primitives

문자열, 숫자, 불리언 및 특수 타입

let name: string = 'Alice';
let age: number = 30;
let active: boolean = true;

// Special types
let data: any = 42;         // escape hatch — avoid
let input: unknown = getData(); // safer than any
let nothing: null = null;
let undef: undefined = undefined;
Tip: any 대신 unknown을 사용하세요. unknown은 타입 체크 없이 사용할 수 없어 더 안전합니다.

Arrays & Tuples

배열 타입과 고정 길이 튜플 타입

// Arrays
const nums: number[] = [1, 2, 3];
const names: Array<string> = ['Alice', 'Bob'];

// Readonly array
const ids: readonly number[] = [1, 2, 3];
// ids.push(4); // Error

// Tuples — fixed length & types
const pair: [string, number] = ['age', 30];
const rgb: [number, number, number] = [255, 128, 0];

// Named tuple (documentation only)
type Range = [start: number, end: number];

Enums

이름 붙은 상수 집합 — 문자열 enum 권장

// String enum (preferred — readable in output)
enum Status {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 'pending',
}

const s: Status = Status.Active;

// Numeric enum (auto-incrementing)
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

// const enum — inlined at compile time
const enum Color { Red, Green, Blue }
Tip: 문자열 enum은 디버깅이 쉽고 직렬화가 간편합니다. 숫자 enum보다 문자열 enum을 권장합니다.

Union & Literal Types

여러 타입 중 하나를 허용하거나 특정 값만 허용

// Union type
type ID = string | number;
function find(id: ID) { /* ... */ }

// Literal types — restrict to specific values
type Theme = 'light' | 'dark' | 'system';
type Dice = 1 | 2 | 3 | 4 | 5 | 6;

let theme: Theme = 'dark'; // OK
// theme = 'blue'; // Error

// Boolean literal
type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };
type Result = Success | Failure;

void / never

void: 반환값 없음, never: 절대 반환하지 않는 함수

// void — no return value
function log(msg: string): void {
  console.log(msg);
}

// never — exhaustive checks / throws
function throwError(msg: string): never {
  throw new Error(msg);
}

// never in exhaustive switch
function handleShape(shape: Circle | Square): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.r ** 2;
    case 'square': return shape.s ** 2;
    default:
      const _: never = shape; // compile error if case missed
      return _;
  }
}
Tip: never 타입을 switch default에 사용하면 새 케이스 추가 시 컴파일 에러로 알려줍니다.

Type Assertions

컴파일러에게 타입을 명시적으로 알려주는 방법

// as syntax (preferred)
const input = document.getElementById('name') as HTMLInputElement;
input.value = 'Hello';

// Angle bracket syntax (not in JSX)
const el = <HTMLDivElement>document.querySelector('.box');

// const assertion — narrows to literal
const config = { endpoint: '/api', retries: 3 } as const;
// type: { readonly endpoint: '/api'; readonly retries: 3 }

// Non-null assertion (use sparingly)
const el2 = document.getElementById('app')!;
🏗️

인터페이스 & 타입

6 items

Interface

객체 구조를 정의하는 계약 — 확장(extends) 및 병합 가능

interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;          // optional
  readonly createdAt: Date;  // immutable after creation
}

// Extend
interface Admin extends User {
  role: 'admin' | 'superadmin';
  permissions: string[];
}

// Declaration merging (unique to interfaces)
interface Window {
  analytics: AnalyticsSDK;
}

Type Alias

타입에 이름을 부여 — 유니온, 튜플, 함수 타입 등에 사용

// Object type
type Point = { x: number; y: number };

// Union (interface cannot do this)
type Result<T> = { ok: true; data: T } | { ok: false; error: string };

// Function type
type Handler = (event: Event) => void;

// Tuple
type Coordinate = [lat: number, lng: number];

// Template literal type
type EventName = `on${'Click' | 'Hover' | 'Focus'}`;
// 'onClick' | 'onHover' | 'onFocus'
Tip: 유니온 타입이 필요하면 type, 객체 구조 선언/확장이 목적이면 interface를 사용하세요.

Intersection (&)

여러 타입을 결합하여 모든 속성을 포함하는 타입 생성

type Timestamps = {
  createdAt: Date;
  updatedAt: Date;
};

type SoftDelete = {
  deletedAt: Date | null;
};

// Combine all
type BaseEntity = Timestamps & SoftDelete & { id: string };

// With generics
type ApiResponse<T> = {
  data: T;
  meta: { page: number; total: number };
} & Timestamps;

Index Signatures

동적 키를 가진 객체의 타입 정의

// String index
interface StringMap {
  [key: string]: string;
}

// Mixed known + dynamic
interface Config {
  name: string;
  version: number;
  [key: string]: string | number | boolean; // catch-all
}

// Record utility (cleaner alternative)
type Env = Record<string, string>;

// Map-like with specific value types
type Translations = Record<'en' | 'ko' | 'ja', string>;

Readonly & Partial Patterns

불변 타입과 선택적 속성 타입 만들기

interface Config {
  host: string;
  port: number;
  debug: boolean;
}

// All readonly
const config: Readonly<Config> = {
  host: 'localhost', port: 3000, debug: false,
};
// config.port = 8080; // Error

// All optional (for updates)
function updateConfig(patch: Partial<Config>) {
  Object.assign(config, patch);
}
updateConfig({ debug: true }); // only pass what changed

// Deep readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
Tip: Partial<T>는 업데이트/패치 함수에, Readonly<T>는 설정 객체에 자주 사용됩니다.

Extending & Composing

인터페이스 상속 및 타입 조합 패턴

// Multiple extends
interface Loggable { log(): void; }
interface Serializable { toJSON(): string; }

interface Entity extends Loggable, Serializable {
  id: string;
}

// Omit to override properties
interface CreateUserDTO extends Omit<User, 'id' | 'createdAt'> {
  password: string;
}

// Pick for subset
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;
🔮

제네릭

6 items

Generic Functions

다양한 타입에서 작동하는 재사용 가능한 함수

// Basic generic function
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

first<string>(['a', 'b']); // type: string
first([1, 2, 3]);          // type: number (inferred)

// Multiple type parameters
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((val, i) => [val, b[i]]);
}

zip(['a', 'b'], [1, 2]); // [['a', 1], ['b', 2]]

Generic Constraints (extends)

제네릭 타입을 특정 구조로 제한

// Must have .length
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest('hello', 'hi');     // 'hello'
longest([1, 2], [1, 2, 3]); // [1, 2, 3]

// Must have .id
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}

// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
Tip: extends로 제약을 걸면 타입 안전성을 유지하면서 유연한 함수를 만들 수 있습니다.

Generic Interfaces & Types

재사용 가능한 제네릭 데이터 구조

// API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// Paginated response
interface Paginated<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}

// Usage
type UserResponse = ApiResponse<User>;
type UserList = Paginated<User>;

async function fetchUsers(): Promise<Paginated<User>> {
  const res = await fetch('/api/users');
  return res.json();
}

Default Generics

제네릭 매개변수에 기본 타입 지정

// Default type parameter
interface Store<T = Record<string, unknown>> {
  get(key: string): T;
  set(key: string, value: T): void;
}

// No type param needed
const store: Store = {
  get: (key) => ({}),
  set: (key, value) => {},
};

// Event emitter with defaults
type EventMap = Record<string, unknown>;
class Emitter<Events extends EventMap = EventMap> {
  on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void) { /* ... */ }
  emit<K extends keyof Events>(event: K, data: Events[K]) { /* ... */ }
}

Generic Classes

타입 안전한 제네릭 클래스

class TypedMap<K, V> {
  private items = new Map<K, V>();

  set(key: K, value: V): void {
    this.items.set(key, value);
  }

  get(key: K): V | undefined {
    return this.items.get(key);
  }

  has(key: K): boolean {
    return this.items.has(key);
  }

  values(): V[] {
    return [...this.items.values()];
  }
}

const cache = new TypedMap<string, User>();
cache.set('alice', { id: 1, name: 'Alice' });
const user = cache.get('alice'); // User | undefined

Utility Types with Generics

제네릭을 활용한 유틸리티 타입 만들기

// Make specific fields optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type CreateUser = Optional<User, 'id' | 'createdAt'>;

// Make specific fields required
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;

// Nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Deep partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Tip: 이런 유틸리티 타입을 프로젝트 전역 types.ts에 정의하면 재사용성이 높아집니다.
🧰

유틸리티 타입

6 items

Partial / Required

Partial: 모든 속성을 선택적으로, Required: 모든 속성을 필수로

interface User {
  name: string;
  email: string;
  age?: number;
}

// Partial — all optional
function updateUser(id: string, patch: Partial<User>) { /* ... */ }
updateUser('1', { name: 'Bob' }); // OK — only name

// Required — all required (removes ?)
const completeUser: Required<User> = {
  name: 'Alice',
  email: 'alice@example.com',
  age: 30, // required now
};

Pick / Omit

Pick: 특정 속성만 선택, Omit: 특정 속성 제외

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Pick — keep only these
type PublicUser = Pick<User, 'id' | 'name'>;
// { id: string; name: string }

// Omit — remove these
type SafeUser = Omit<User, 'password'>;
// { id, name, email, createdAt }

// Combine for DTOs
type CreateUserDTO = Omit<User, 'id' | 'createdAt'>;
type UpdateUserDTO = Partial<Omit<User, 'id'>>;
Tip: API DTO 정의 시 Pick/Omit으로 기존 타입에서 파생하면 타입 동기화가 보장됩니다.

Record

키-값 쌍의 객체 타입을 간결하게 정의

// String → value mapping
type Env = Record<string, string>;
const env: Env = { NODE_ENV: 'production', PORT: '3000' };

// Union key → value
type StatusMap = Record<'active' | 'inactive' | 'pending', User[]>;

// Enum key
type ThemeColors = Record<Theme, { bg: string; text: string }>;

// Common pattern: lookup table
const statusLabels: Record<Status, string> = {
  active: 'Active',
  inactive: 'Inactive',
  pending: 'Pending',
};

Exclude / Extract

Exclude: 유니온에서 타입 제거, Extract: 유니온에서 타입 추출

type Events = 'click' | 'scroll' | 'mousemove' | 'keypress';

// Remove from union
type MouseEvents = Exclude<Events, 'keypress'>;
// 'click' | 'scroll' | 'mousemove'

// Keep matching
type KeyEvents = Extract<Events, 'keypress'>;
// 'keypress'

// Practical: filter union types
type Shape = Circle | Square | Triangle;
type RoundShape = Extract<Shape, { kind: 'circle' }>;

// NonNullable = Exclude<T, null | undefined>
type Name = NonNullable<string | null | undefined>;
// string

ReturnType / Parameters

함수의 반환 타입 또는 매개변수 타입 추출

function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age, active: true };
}

// Extract return type
type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number; active: boolean }

// Extract parameter types
type CreateParams = Parameters<typeof createUser>;
// [name: string, age: number]

// Awaited — unwrap Promise
type Data = Awaited<Promise<Promise<string>>>;
// string

// ConstructorParameters
type DateArgs = ConstructorParameters<typeof Date>;
Tip: ReturnType은 서드파티 라이브러리 함수의 반환 타입을 타입으로 쓰고 싶을 때 유용합니다.

Awaited / InstanceType

Awaited: Promise 언래핑, InstanceType: 클래스 인스턴스 타입

// Awaited — deeply unwraps Promise
type A = Awaited<Promise<string>>;          // string
type B = Awaited<Promise<Promise<number>>>; // number

async function fetchData() { return { users: [] as User[] }; }
type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { users: User[] }

// InstanceType
class Logger {
  log(msg: string) { console.log(msg); }
}
type LoggerInstance = InstanceType<typeof Logger>;

function createService<T extends new (...args: any[]) => any>(
  Cls: T
): InstanceType<T> {
  return new Cls();
}
🛡️

타입 가드

6 items

typeof / instanceof

런타임 타입 체크로 타입 좁히기

// typeof
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase(); // TS knows: string
  }
  return value.toFixed(2);      // TS knows: number
}

// instanceof
function getArea(shape: Circle | Rectangle) {
  if (shape instanceof Circle) {
    return Math.PI * shape.radius ** 2; // Circle
  }
  return shape.width * shape.height;     // Rectangle
}

Discriminated Unions

공통 판별 속성으로 유니온 타입을 구분하는 패턴

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'rectangle'; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':    return Math.PI * shape.radius ** 2;
    case 'square':    return shape.side ** 2;
    case 'rectangle': return shape.width * shape.height;
  }
}

// API result pattern
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult(r: Result<User>) {
  if (r.success) {
    console.log(r.data.name); // narrowed to success
  } else {
    console.error(r.error);   // narrowed to failure
  }
}
Tip: 판별 유니온은 Redux 액션, API 응답, 상태 머신 등에서 가장 많이 사용되는 TypeScript 패턴입니다.

Type Predicates (is)

사용자 정의 타입 가드 함수

// is keyword narrows the type
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isUser(obj: unknown): obj is User {
  return (
    obj !== null &&
    typeof obj === 'object' &&
    'id' in obj &&
    'name' in obj
  );
}

// Usage — TS narrows after check
const items: unknown[] = [1, 'hello', { id: 1, name: 'Alice' }];
const strings = items.filter(isString);
// type: string[]
Tip: filter와 함께 타입 가드를 사용하면 배열의 타입을 자동으로 좁힐 수 있습니다.

in Operator

속성 존재 여부로 타입 좁히기

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim(); // TS knows: Fish
  } else {
    animal.fly();  // TS knows: Bird
  }
}

// With optional properties
type Admin = { role: string; permissions: string[] };
type Guest = { sessionId: string };

function greet(user: Admin | Guest) {
  if ('role' in user) {
    console.log(`Admin: ${user.role}`);
  }
}

satisfies Operator

타입을 검증하면서 추론된 타입을 유지 (TS 4.9+)

// Without satisfies — type widens
const routes: Record<string, string> = {
  home: '/', about: '/about', contact: '/contact',
};
routes.home; // type: string (can't narrow)

// With satisfies — validates but keeps literal types
const routes2 = {
  home: '/',
  about: '/about',
  contact: '/contact',
} satisfies Record<string, string>;

routes2.home; // type: '/' (literal preserved!)
// routes2.missing; // Error — key doesn't exist
Tip: satisfies는 타입을 확인하면서도 타입 추론의 이점을 유지합니다. as const와 함께 사용하면 더 강력합니다.

Assertion Functions

조건이 거짓이면 예외를 던지는 타입 단언 함수

function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`${name} is not defined`);
  }
}

// After assertion, TS narrows the type
const user = getUser(); // User | null
assertDefined(user, 'user');
console.log(user.name); // TS knows: User (not null)

function assertString(val: unknown): asserts val is string {
  if (typeof val !== 'string') throw new TypeError('Expected string');
}

고급 타입

6 items

Conditional Types

조건에 따라 다른 타입을 반환하는 타입 수준 삼항 연산

// Basic conditional
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>;      // false

// Practical: unwrap array
type Flatten<T> = T extends (infer U)[] ? U : T;
type Num = Flatten<number[]>;  // number
type Str = Flatten<string>;    // string

// Distributive conditional
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined>;
// string

Mapped Types

기존 타입의 각 속성을 변환하여 새 타입 생성

// Make all properties optional (Partial implementation)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Transform property types
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
Tip: as 절을 사용하면 매핑 중 키 이름을 변환할 수 있습니다 (TS 4.1+).

Template Literal Types

문자열 리터럴 타입을 조합하여 새 타입 생성

// Basic concatenation
type Greeting = `Hello, ${'World' | 'TypeScript'}`;
// 'Hello, World' | 'Hello, TypeScript'

// Event name generation
type EventName = `${'click' | 'scroll' | 'keydown'}Handler`;
// 'clickHandler' | 'scrollHandler' | 'keydownHandler'

// CSS property pattern
type CSSUnit = `${number}${'px' | 'rem' | 'em' | '%'}`;
const width: CSSUnit = '100px'; // OK
// const bad: CSSUnit = '100vw'; // Error

// Intrinsic string manipulation
type Upper = Uppercase<'hello'>;     // 'HELLO'
type Lower = Lowercase<'HELLO'>;     // 'hello'
type Cap = Capitalize<'hello'>;      // 'Hello'
type Uncap = Uncapitalize<'Hello'>; // 'hello'

infer Keyword

조건부 타입 내에서 타입 변수를 추론하여 추출

// Extract array element type
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Item = ElementOf<string[]>; // string

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Data = Unwrap<Promise<{ users: User[] }>>;
// { users: User[] }

// Extract function return type (ReturnType impl)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract first argument
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Arg = FirstArg<(name: string, age: number) => void>;
// string

keyof / typeof

keyof: 객체 키의 유니온, typeof: 값에서 타입 추출

interface User {
  id: number;
  name: string;
  email: string;
}

type UserKeys = keyof User; // 'id' | 'name' | 'email'

// typeof — extract type from a value
const config = {
  api: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
};

type Config = typeof config;
// { api: string; timeout: number; retries: number }

// Combine keyof + typeof
type ConfigKey = keyof typeof config;
// 'api' | 'timeout' | 'retries'

function getSetting(key: keyof typeof config) {
  return config[key];
}

as const

값을 가장 좁은 리터럴 타입으로 고정

// Without as const
const colors = ['red', 'green', 'blue'];
// type: string[]

// With as const
const colors2 = ['red', 'green', 'blue'] as const;
// type: readonly ['red', 'green', 'blue']

type Color = (typeof colors2)[number];
// 'red' | 'green' | 'blue'

// Object as const
const STATUS = {
  ACTIVE: 'active',
  INACTIVE: 'inactive',
} as const;

type StatusValue = (typeof STATUS)[keyof typeof STATUS];
// 'active' | 'inactive'
Tip: as const + typeof 조합은 enum 없이 타입 안전한 상수를 만드는 가장 좋은 방법입니다.
🔧

함수 & 클래스

5 items

Function Overloads

같은 함수에 여러 시그니처 정의 — 입력에 따라 반환 타입 변경

// Overload signatures
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: string): HTMLElement;

// Implementation signature
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const input = createElement('input');  // HTMLInputElement
const canvas = createElement('canvas'); // HTMLCanvasElement
const div = createElement('div');      // HTMLElement

Generic Functions

타입 안전한 범용 함수 패턴

// Type-safe object getter
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 30 };
const name = get(user, 'name');  // string
const age = get(user, 'age');    // number
// get(user, 'foo'); // Error — 'foo' not in keyof User

// Type-safe API wrapper
async function fetchJSON<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

const users = await fetchJSON<User[]>('/api/users');

Abstract Classes

추상 클래스 — 서브클래스가 반드시 구현해야 하는 계약

abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;

  // Concrete method
  describe(): string {
    return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area() { return Math.PI * this.radius ** 2; }
  perimeter() { return 2 * Math.PI * this.radius; }
}

// const s = new Shape(); // Error — cannot instantiate abstract
const c = new Circle(5);
c.describe(); // 'Area: 78.54, Perimeter: 31.42'

Access Modifiers

public, private, protected, readonly 접근 제어자

class BankAccount {
  public owner: string;
  private balance: number;
  protected currency: string;
  readonly accountId: string;

  constructor(owner: string, initial: number) {
    this.owner = owner;
    this.balance = initial;
    this.currency = 'USD';
    this.accountId = crypto.randomUUID();
  }

  // Parameter properties shorthand
  // constructor(
  //   public owner: string,
  //   private balance: number,
  // ) {}

  getBalance(): number { return this.balance; }
}
Tip: 생성자 매개변수에 접근 제어자를 붙이면 자동으로 속성이 선언되어 코드가 간결해집니다.

implements

클래스가 인터페이스 계약을 준수하는지 검증

interface Logger {
  info(message: string): void;
  error(message: string, error?: Error): void;
  warn(message: string): void;
}

class ConsoleLogger implements Logger {
  info(message: string) { console.log(`[INFO] ${message}`); }
  error(message: string, error?: Error) {
    console.error(`[ERROR] ${message}`, error);
  }
  warn(message: string) { console.warn(`[WARN] ${message}`); }
}

// Multiple interfaces
interface Serializable { serialize(): string; }
interface Cacheable { cacheKey(): string; }

class UserModel implements Serializable, Cacheable {
  serialize() { return JSON.stringify(this); }
  cacheKey() { return `user:${this.id}`; }
}
💡

패턴 & 팁

6 items

Exhaustive Check (never)

switch/if에서 모든 케이스를 처리했는지 컴파일 타임에 검증

type Status = 'active' | 'inactive' | 'pending';

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function getLabel(status: Status): string {
  switch (status) {
    case 'active':   return 'Active';
    case 'inactive': return 'Inactive';
    case 'pending':  return 'Pending';
    default:         return assertNever(status);
    // If you add 'archived' to Status, this will error
    // until you add its case
  }
}
Tip: 유니온에 새 값을 추가하면 모든 switch 문에서 컴파일 에러가 발생하여 누락을 방지합니다.

Branded Types

같은 기본 타입이지만 의미적으로 구분되는 타입 만들기

// Prevent mixing up IDs
type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };

function createUserId(id: string): UserId { return id as UserId; }
function createPostId(id: string): PostId { return id as PostId; }

function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }

const userId = createUserId('user-123');
const postId = createPostId('post-456');

getUser(userId); // OK
// getUser(postId); // Error — PostId is not UserId
Tip: 브랜드 타입은 실제 런타임 비용이 없습니다. 순수하게 컴파일 타임 안전장치입니다.

Builder Pattern

체이닝 API로 타입 안전한 객체 생성

class QueryBuilder<T extends Record<string, any>> {
  private query: Partial<T> = {};

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.query[key] = value;
    return this;
  }

  build(): Partial<T> {
    return { ...this.query };
  }
}

interface UserQuery {
  name: string;
  age: number;
  active: boolean;
}

const query = new QueryBuilder<UserQuery>()
  .where('name', 'Alice')
  .where('active', true)
  // .where('name', 42) // Error — 42 is not string
  .build();

Type-safe Event Emitter

이벤트 이름과 페이로드를 타입으로 검증하는 이벤트 시스템

type EventMap = {
  login: { userId: string; timestamp: number };
  logout: { userId: string };
  error: { message: string; code: number };
};

class TypedEmitter<T extends Record<string, any>> {
  private handlers = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event)!.add(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.handlers.get(event)?.forEach(h => h(data));
  }
}

const events = new TypedEmitter<EventMap>();
events.on('login', ({ userId }) => console.log(userId));
// events.emit('login', { message: '' }); // Error

Zod Integration

Zod 스키마에서 TypeScript 타입 자동 추론

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['admin', 'user', 'guest']),
});

// Infer type from schema
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; age?: number; role: 'admin' | 'user' | 'guest' }

// Runtime validation
const result = UserSchema.safeParse(input);
if (result.success) {
  console.log(result.data.name); // fully typed
} else {
  console.error(result.error.issues);
}
Tip: Zod를 사용하면 런타임 검증과 컴파일 타임 타입을 하나의 스키마로 관리할 수 있습니다.

Module Augmentation

기존 모듈이나 전역 타입을 확장

// Extend Express Request
declare module 'express' {
  interface Request {
    user?: { id: string; role: string };
    requestId: string;
  }
}

// Extend Window
declare global {
  interface Window {
    __APP_VERSION__: string;
    analytics: {
      track(event: string, data?: Record<string, unknown>): void;
    };
  }
}

// Extend existing enum
declare module './types' {
  interface UserRoles {
    superadmin: 'superadmin';
  }
}

export {}; // Make this a module

TypeScript 치트시트 사용 가이드

TypeScript는 JavaScript에 정적 타입 시스템을 추가하여 개발 생산성과 코드 품질을 크게 향상시킵니다. 이 치트시트는 실무에서 가장 자주 필요한 타입 패턴을 빠르게 참고할 수 있도록 정리했습니다.

제네릭은 왜 중요한가요?

제네릭은 타입 정보를 잃지 않으면서 재사용 가능한 코드를 작성하는 핵심 도구입니다. API 응답 래퍼, 컬렉션 유틸리티, 상태 관리 등에서 any 대신 제네릭을 사용하면 자동완성과 타입 체크의 혜택을 동시에 받을 수 있습니다.

유틸리티 타입 활용하기

Partial, Pick, Omit, Record 등의 내장 유틸리티 타입은 기존 타입에서 새 타입을 파생할 때 필수적입니다. 예를 들어 API의 Create DTO는 Omit<User, "id" | "createdAt">로, Update DTO는 Partial<Omit<User, "id">>로 간결하게 정의할 수 있습니다.

타입 안전성을 높이는 패턴

판별 유니온, 브랜드 타입, 철저한 검사(exhaustive check) 등의 패턴을 활용하면 런타임 에러를 컴파일 타임에 미리 잡을 수 있습니다. 특히 switch 문에서 never 타입을 활용한 철저한 검사는 새로운 케이스 추가 시 누락을 자동으로 감지해 줍니다.

FAQ

Related Tools

Also Used Together