Skip to content

TypeScript 치트시트

확장 가능한 JavaScript의 타입화된 상위 집합.

01

시작하기

Hello World & 컴파일

TypeScript 파일은 .ts 확장자를 사용합니다. tsc 컴파일러는 TS를 JS로 트랜스파일하며, 런타임에 모든 타입 어노테이션을 지웁니다. 최대 타입 안전을 위해 --strict를 사용하세요. ts-node나 bun은 별도의 컴파일 단계 없이 .ts 파일을 직접 실행할 수 있습니다.

typescript
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);

// Compile to JavaScript (type annotations erased):
//   tsc hello.ts          -> hello.js
//   tsc --strict hello.ts  // enable all strict checks
//   tsc --watch hello.ts   // recompile on change

// Run directly with ts-node or bun:
//   ts-node hello.ts

tsconfig.json

tsconfig.json은 TypeScript 컴파일러를 설정합니다. 'strict: true'는 noImplicitAny, strictNullChecks, strictFunctionTypes 등을 활성화합니다. 'target'은 출력 JS 버전을 제어합니다. 'esModuleInterop'은 Node의 내장 모듈과 같은 CommonJS 모듈에서 기본 임포트를 활성화합니다.

typescript
{
  "compilerOptions": {
    "target": "ES2020",        // JS version to emit
    "module": "ESNext",        // module system
    "strict": true,            // enable all strict checks
    "outDir": "./dist",        // output directory
    "rootDir": "./src",        // source root
    "esModuleInterop": true,   // allow default imports from CJS
    "skipLibCheck": true,      // skip .d.ts type checking
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

타입 어노테이션 & 타입 추론

타입 어노테이션은 변수의 타입을 명시적으로 지정합니다. TypeScript는 값에서 타입을 추론할 수도 있습니다. 함수 시그니처와 공개 API에는 명시적 어노테이션을 사용하고, 지역 변수에는 추론에 의존하세요. 'any'는 피하세요 — 타입 검사를 완전히 건너뜁니다.

typescript
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];

// Type inference (let TS figure it out)
let age = 30;           // inferred as number
let items = [1, 2, 3];  // inferred as number[]
let mixed = [1, "a"];   // inferred as (string | number)[]

// any disables type checking (avoid!)
let anything: any = 42;
anything = "string";    // no error

엄격 모드 검사

엄격 모드는 중요한 검사를 활성화합니다: strictNullChecks (null/undefined를 다른 타입에 할당 불가), noImplicitAny (매개변수에 타입 필요), strictPropertyInitialization (클래스 필드 초기화 필요). 필드가 나중에 설정될 것이라 확신할 때 '!' (확정 할당)을 사용하세요.

typescript
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode

// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any

// strictPropertyInitialization
class User {
  name: string; // Error: not initialized
  constructor() {}
}

// Fix: initialize or use definite assignment
class Fixed {
  name!: string; // definite assignment assertion
  age: number = 0;
}

선언 파일 (.d.ts)

선언 파일(.d.ts)은 TypeScript 정의가 없는 JavaScript 라이브러리에 타입을 제공합니다. 'declare'는 변수/함수가 런타임에 존재함을 컴파일러에게 알립니다. 인기 있는 라이브러리를 위해 DefinitelyTyped의 @types 패키지를 사용하세요 (예: @types/node, @types/react).

typescript
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
  export function greet(name: string): string;
  export const version: string;
}

// global.d.ts - extend global scope
declare global {
  interface Window {
    myApp: { version: string };
  }
}

// Usage in .ts files:
// import { greet } from "my-lib";  // now typed
// Install @types packages: npm i -D @types/node @types/react
02

기본 타입

원시 타입 & 특수 타입

TypeScript 원시 타입: string, number, boolean, bigint, symbol. 'void'는 함수가 값을 반환하지 않음을 나타냅니다. 'never'는 절대 발생하지 않는 값을 나타냅니다 — 예외를 던지거나 영원히 실행되는 함수입니다. switch 문에서 철저한 검사를 위해 'never'를 사용하세요.

typescript
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");

// void: function returns nothing
function log(msg: string): void { console.log(msg); }

// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }

배열 & 튜플

배열은 T[] 또는 Array<T> 구문을 사용합니다. ReadonlyArray는 변경을 방지합니다. 튜플은 각 인덱스에 특정 타입을 갖는 고정 길이 배열입니다 — 키-값 쌍이나 CSV 형식 데이터에 유용합니다. 레이블이 있는 튜플은 명명된 위치로 가독성을 향상합니다.

typescript
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable

// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1];  // number

// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];

열거형(Enum)

열거형은 명명된 상수 집합을 정의합니다. 문자열 열거형은 디버깅에 권장됩니다 (출력에서 값이 읽기 쉬움). 숫자 열거형은 역방향 매핑을 지원합니다. 'const enum'은 컴파일 타임에 지워집니다 (인라인됨) — 런타임 비용이 없습니다. 간단한 경우에는 유니언 타입을 선호하세요.

typescript
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up;  // 0

// String enum (recommended for readability)
enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"

// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

any vs unknown vs never

'any'는 모든 타입 검사를 비활성화합니다 — 피하세요. 'unknown'은 타입 안전한 대안입니다: 사용하기 전에 반드시 좁혀야 합니다 (typeof, instanceof 사용). 'never'는 절대 발생하지 않는 값을 나타내며, switch 문에서 컴파일 타임에 누락된 케이스를 잡기 위한 철저성 검사에 사용됩니다.

typescript
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string";       // OK
a.toUpperCase();    // OK (no check, may fail at runtime)

// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
  u.toUpperCase();  // OK after narrowing
}

// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return Math.PI;
    case "square": return 1;
    default:
      const _exhaustive: never = s; // Error if case missing
  }
}

타입 어설션

타입 어설션은 컴파일러에게 '내가 타입을 알아, 믿어'라고 알립니다. 'as' 구문을 사용하세요. non-null 어설션(!)은 TS에 값이 null/undefined가 아님을 알립니다. 'as const'는 모든 속성을 읽기 전용 리터럴로 만듭니다 — 설정 객체와 Redux 액션 타입에 유용합니다. 어설션은 런타임 동작을 변경하지 않습니다.

typescript
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;

// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi";  // el is HTMLElement, not null

// Double assertion (for unsafe casts)
let value = "42" as unknown as number;

// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly

리터럴 & 유니언 타입

리터럴 타입은 값을 특정 문자열, 숫자, 또는 불리언으로 제한합니다. 유니언과 결합하여 방향이나 HTTP 메서드 같은 정밀한 타입을 만듭니다. 템플릿 리터럴 타입(TS 4.1+)은 다른 타입에서 문자열 타입을 구축합니다 — 타입 안전한 키와 이벤트 이름 생성에 강력합니다.

typescript
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left";  // OK
// direction = "sideways"; // Error

// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4;  // OK

// Boolean literal
let flag: true = true;

// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
03

인터페이스 & 객체

인터페이스 기본

인터페이스는 객체의 형태를 설명합니다. '?'는 선택적 속성(undefinded일 수 있음)을 표시합니다. 'readonly'는 초기화 후 재할당을 방지합니다. 인터페이스는 컴파일 타임 전용입니다 — 출력 JavaScript에서 지워집니다. 객체와 클래스에 대한 계약을 정의하는 데 사용하세요.

typescript
interface User {
  id: number;
  name: string;
  email?: string;        // optional property
  readonly createdAt: Date; // immutable
}

const user: User = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined

인덱스 시그니처

인덱스 시그니처는 주어진 타입의 임의 키를 가진 객체를 허용합니다. 모든 속성 값은 인덱스 타입에 할당 가능해야 합니다. 딕셔너리, 캐시, 동적 데이터에 유용합니다. 알려진 속성과 결합하여 추가 옵션이 있는 타입화된 설정에 사용하세요.

typescript
// Index signature: arbitrary string keys
interface StringMap {
  [key: string]: string;
}

const dict: StringMap = {
  name: "Alice",
  city: "NYC",
  // count: 42, // Error: value must be string
};

// Mixed: known + index signature
interface Config {
  name: string;
  [key: string]: string | number;
}

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}

인터페이스 확장

인터페이스는 하나 이상의 다른 인터페이스를 확장하여 멤버를 결합할 수 있습니다. 이를 통해 컴포지션과 코드 재사용이 가능합니다. 클래스와 달리 인터페이스는 다중 상속을 지원합니다. 인터페이스를 구현할 때, 클래스는 모든 필수 멤버를 제공해야 합니다.

typescript
interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

interface Swimmer {
  swim(): void;
}

// Multiple inheritance
interface Duck extends Pet, Swimmer {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  owner: "Walt",
  eat() {},
  play() {},
  swim() {},
  quack() {},
};

인터페이스의 함수 타입

인터페이스는 함수 시그니처를 설명할 수 있어 타입 안전한 콜백이 가능합니다. 하이브리드 인터페이스(호출 가능 + 속성)는 메서드도 있는 jQuery 스타일 함수에 사용됩니다. 이 패턴은 도우미가 첨부된 함수를 반환하는 라이브러리에서 흔합니다.

typescript
interface SearchFn {
  (source: string, keyword: string): boolean;
}

const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true

// Interface with mixed members (hybrid)
interface Counter {
  (start: number): void;  // callable
  count: number;           // property
  reset(): void;           // method
}

// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };

인터페이스 vs 타입 별칭

인터페이스는 선언 병합(같은 이름의 인터페이스 결합), 더 나은 에러 메시지를 지원하며 객체/클래스 형태에 권장됩니다. 타입 별칭은 더 유연하지만(유니언, 원시 타입, 튜플 표현 가능) 병합할 수 없습니다. 확장 가능한 API에는 인터페이스를, 유니언과 계산된 타입에는 타입 별칭을 사용하세요.

typescript
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };

// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;

// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };

// Use interface for objects/classes, type for unions/aliases

옵셔널 체이닝 & 널 병합

옵셔널 체이닝(?.)은 중첩된 속성에 안전하게 접근합니다 — 어떤 연결고리라도 null/undefined이면 예외를 던지는 대신 undefined를 반환합니다. 널 병합(??)은 null/undefined에 대해서만 기본값을 제공합니다 (0이나 ''이 아님). 이 연산자들은 장황한 null 검사 코드를 크게 줄입니다.

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

const user: User = {};

// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined

// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";

// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky

// Optional method call
const result = user.profile?.address?.city?.toUpperCase();
04

타입 별칭 & 유니언

타입 별칭

타입 별칭은 유니언, 인터섹션, 원시 타입, 제네릭을 포함한 모든 타입에 대한 명명된 참조를 만듭니다. 인터페이스와 달리 별칭은 병합되거나 확장될 수 없지만, 더 유연합니다. 유니언, 튜플, 유틸리티 타입에는 별칭을 사용하고, 객체 형태에는 인터페이스를 사용하세요.

typescript
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };

// Generic alias
type Container<T> = { value: T };

// Function type alias
type Handler<T> = (event: T) => void;

// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);

유니언 타입

유니언 타입(A | B)은 값이 여러 타입 중 하나일 수 있게 합니다. TypeScript는 typeof, instanceof, 또는 in 검사를 사용하여 조건 블록 내에서 타입을 좁힙니다. 참고: (string | number)[]는 string[] | number[]와 다릅니다 — 전자는 혼합 배열, 후자는 모두 문자열이거나 모두 숫자입니다.

typescript
// Union: value can be one of several types
type ID = string | number;

function display(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // narrowed to string
  } else {
    console.log(id.toFixed(2));    // narrowed to number
  }
}

display("abc");  // ABC
display(42);     // 42.00

// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];

인터섹션 타입

인터섹션 타입(A & B)은 여러 타입의 모든 멤버를 결합합니다 — 결과는 모든 타입을 만족해야 합니다. 믹스인, 컴포지션, 유틸리티 타입 병합에 유용합니다. 유니언(OR)과 달리 인터섹션은 AND입니다: 값은 모든 타입의 모든 속성을 가져야 합니다.

typescript
// Intersection: combine multiple types into one
interface BusinessPartner {
  name: string;
  credit: number;
}

interface Identity {
  id: number;
  email: string;
}

type Employee = BusinessPartner & Identity;

const emp: Employee = {
  name: "Alice",
  credit: 1000,
  id: 1,
  email: "[email protected]",
};

// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props

널러블 타입

엄격 모드에서 null과 undefined는 다른 타입에 할당할 수 없습니다 — 유니언(string | null)으로 명시적으로 포함해야 합니다. 선택적 매개변수(param?)는 암시적으로 T | undefined입니다. 안전한 기본값에는 ??를, non-null 단언에는 !(드물게 사용)을 사용하세요.

typescript
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice";  // OK

// Optional parameters are implicitly | undefined
function greet(name?: string) {
  // name is string | undefined
  console.log(name ?? "Guest");
}

// Return type can be null
function find(id: number): string | null {
  return id === 1 ? "Alice" : null;
}

// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"

Keyof & Typeof 연산자

'keyof T'는 타입 T의 키를 문자열 리터럴 유니언으로 추출합니다. 'typeof x'는 값의 타입을 추출합니다(객체에서 추론하는 데 유용). 'keyof typeof obj'는 둘을 결합하여 기존 객체의 키를 얻습니다 — Redux 액션 타입과 타입 안전한 속성 접근자에서 흔합니다.

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

// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"

function getProp(obj: User, key: keyof User) {
  return obj[key];
}

// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }

// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"

매핑된 타입

매핑된 타입은 키를 순회하며 타입을 변환합니다. Readonly, Partial, Pick 같은 내장 유틸리티가 매핑된 타입입니다. + 와 - 수정자를 사용하여 readonly 또는 선택적을 추가/제거하세요. 키 리매핑(TS 4.1+)은 템플릿 리터럴 타입을 사용하여 키 이름을 바꿉니다 — getter/setter 타입 생성에 강력합니다.

typescript
// Map over keys to create a new type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

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

type ReadonlyUser = Readonly<User>;   // all readonly
type OptionalUser = Optional<User>;   // all optional

// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };

// Key remapping (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
05

함수

함수 타입 & 시그니처

TypeScript는 함수 매개변수와 반환 값에 타입 어노테이션을 추가합니다. 반환 타입은 종종 추론 가능하지만, 공개 API에는 명시적 어노테이션이 권장됩니다. 기본 매개변수는 대체 값으로 인수를 선택적으로 만듭니다. 함수가 아무것도 반환하지 않을 때 void를 사용하세요.

typescript
// Named function with types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;

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

// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
  return `${greeting}, ${name}!`;
}
greet("Alice");        // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"

나머지 매개변수 & 튜플

나머지 매개변수(...args)는 여러 인수를 배열로 수집합니다. TypeScript는 이를 T[] 또는 고정 길이 가변 함수를 위한 튜플로 타입화합니다. 스프레드 연산자(...)는 반대로 동작합니다 — 배열을 개별 인수로 확장합니다. 튜플 나머지 타입은 정밀한 가변 시그니처를 가능하게 합니다.

typescript
// Rest parameters (variadic)
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
  console.log(name, scores);
}

// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));

// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
  const [s, n, b] = args;
}

함수 오버로드

함수 오버로드는 같은 함수에 여러 타입 시그니처를 제공하여, 입력에 따른 정밀한 반환 타입을 가능하게 합니다. 구현 시그니처는 호출자에게 숨겨집니다. 오버로드는 위에서 아래로 해결됩니다 — 더 구체적인 시그니처를 먼저 배치하세요. jQuery와 lodash 같은 라이브러리에서 흔합니다.

typescript
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return [input, input * 2];
}

const strs = parse("a,b,c"); // string[]
const nums = parse(42);      // number[]

// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
  return m === undefined
    ? new Date(yOrTs)
    : new Date(yOrTs, m - 1, d);
}

this 타입

TypeScript는 'this' 타입을 첫 번째 매개변수로 선언할 수 있게 합니다. 이는 함수가 올바른 컨텍스트로 호출됨을 보장합니다 — 콜백으로 전달되는 메서드에 유용합니다. 화살표 함수는 'this'를 렉시컬하게 캡처하여 .bind()의 필요를 피합니다. 타입이 없는 'this' 에러를 잡으려면 'noImplicitThis'를 사용하세요.

typescript
interface Card {
  suit: string;
  rank: string;
  isFaceUp(): boolean;
}

// Explicit 'this' parameter
function format(this: Card): string {
  return `${this.rank} of ${this.suit}`;
}

const card: Card = {
  suit: "Hearts",
  rank: "A",
  isFaceUp() { return true; },
  format,
};

// 'this' in callbacks with bind
class Handler {
  private count = 0;
  increment = () => { this.count++; }; // arrow binds this
}

콜백 & 고차 함수

TypeScript는 고차 함수(함수를 받거나 반환하는 함수)를 완전히 타입화합니다. 입력과 출력 간의 타입 관계를 보존하려면 제네릭 타입 매개변수(T, U)를 사용하세요. 콜백 타입은 재사용을 위해 타입 별칭으로 정의하는 것이 일반적입니다. 커링(함수 반환)은 완전히 타입 안전합니다.

typescript
// Function as parameter
type Callback<T> = (value: T, index: number) => void;

function forEach<T>(arr: T[], cb: Callback<T>): void {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
  }
}

forEach(["a", "b"], (v, i) => console.log(i, v));

// Function returning function (curry)
function add(a: number): (b: number) => number {
  return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8

// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  return arr.map(fn);
}

매개변수 구조 분해

TypeScript는 함수 매개변수에서 구조 분해를 지원합니다 — 분해된 형태를 인라인으로 또는 인터페이스를 통해 어노테이션합니다. 인터페이스로 추출하면 가독성과 재사용성이 향상됩니다. 배열/튜플 구조 분해도 작동합니다. 이 패턴은 React 컴포넌트 props와 API 핸들러에서 흔합니다.

typescript
// Destructured parameters with types
function createUser({ name, age, email }: {
  name: string;
  age: number;
  email?: string;
}): void {
  console.log(name, age, email);
}

createUser({ name: "Alice", age: 30 });

// Extract to interface for reuse
interface UserOpts {
  name: string;
  age: number;
  email?: string;
}

function updateUser({ name, age }: UserOpts): void {}

// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
  return [b, a];
}
06

클래스 & OOP

클래스 & 생성자

TypeScript 클래스는 매개변수 속성을 지원합니다 — 생성자 매개변수에 접근 수정자(public/private/protected/readonly)를 붙이면 필드가 자동 생성되고 할당됩니다. 이 약식은 보일러플레이트를 줄입니다. 메서드는 반환 값에 타입 어노테이션을 가질 수 있습니다. 필드는 기본이 public입니다.

typescript
class Person {
  // Parameter properties (shorthand)
  constructor(
    public name: string,    // auto-creates this.name
    private age: number,    // private field
    readonly id: number,    // immutable
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const p = new Person("Alice", 30, 1);
console.log(p.name);  // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly

접근 수정자

접근 수정자: public(기본, 어디서나), private(클래스만), protected(클래스 + 서브클래스), readonly(불변). TypeScript의 'private'는 컴파일 타임 전용입니다; ES '#' 프라이빗 필드는 런타임에 진정으로 private입니다. 구현 세부사항에는 private을, 확장 지점에는 protected를 사용하세요.

typescript
class BankAccount {
  public owner: string;       // accessible everywhere
  private balance: number;    // class only
  protected rate: number;     // class + subclasses
  readonly id: string;        // immutable after init
  #secret: string;            // ES private (runtime)

  constructor(owner: string) {
    this.owner = owner;
    this.balance = 0;
    this.rate = 0.05;
    this.id = crypto.randomUUID();
    this.#secret = "hidden";
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

상속 & 추상 클래스

추상 클래스는 직접 인스턴스화할 수 없습니다 — 서브클래스의 기반을 정의합니다. 추상 메서드는 기반 클래스에 구현이 없습니다; 서브클래스가 구현해야 합니다. 상속에는 'extends'를, 부모 생성자 호출에는 'super()'를 사용하세요. 추상 클래스는 다형성을 가능하게 합니다 — 코드가 모든 Shape 서브클래스와 작동할 수 있습니다.

typescript
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;  // must be implemented
  describe(): string {
    return `${this.color} shape, area ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private r: number) {
    super(color);
  }
  area(): number { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(color: string, private side: number) {
    super(color);
  }
  area(): number { return this.side ** 2; }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract

인터페이스 & 구현

클래스는 여러 인터페이스를 구현할 수 있습니다(쉼표로 구분). 클래스는 모든 인터페이스 멤버를 제공해야 합니다. extends(단일 상속)와 달리 implements는 여러 계약을 지원합니다. 이것이 TypeScript가 다중 상속과 유사한 동작을 달성하는 방법입니다. 계약은 인터페이스로, 구현은 클래스로 정의하세요.

typescript
interface Printable {
  toString(): string;
}

interface Comparable<T> {
  compareTo(other: T): number;
}

class Money implements Printable, Comparable<Money> {
  constructor(private amount: number) {}

  toString(): string {
    return `$${this.amount.toFixed(2)}`;
  }

  compareTo(other: Money): number {
    return this.amount - other.amount;
  }
}

const a = new Money(10);
const b = new Money(20);
console.log(a.toString());      // "$10.00"
console.log(a.compareTo(b));    // -10

게터 & 세터

게터와 세터는 검증, 계산, 또는 부작용을 위해 속성 접근을 가로챕니다. private 백킹 필드를 사용하세요(관례: 언더스코어 접두사). 게터는 계산된 속성을 가능하게 합니다(섭씨에서 화씨처럼). 세터는 검증을 가능하게 합니다. 일반 속성처럼 접근하세요 — 괄호 없이.

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) throw new Error("Below absolute zero");
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws

정적 멤버 & 싱글톤

정적 멤버는 인스턴스가 아닌 클래스에 속합니다 — ClassName.member로 접근합니다. 상수, 유틸리티 함수, 팩토리 메서드에 static을 사용하세요. private 생성자 + static getInstance()가 싱글톤 패턴을 구현합니다. 'as const'는 정적 배열을 리터럴 타입으로 읽기 전용으로 만듭니다.

typescript
class Logger {
  static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent direct new

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;

  log(msg: string): void {
    this.logs.push(msg);
  }
}

const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor
07

제네릭

제네릭 함수

제네릭(<T>)은 타입 안전성을 보존하면서 모든 타입과 작동하는 함수를 작성하게 합니다. 타입 매개변수 T는 호출 시간에 채워지는 자리표시자입니다 — 명시적으로(identity<number>) 또는 인수에서 추론됩니다. 제네릭은 재사용 가능하고 타입 안전한 데이터 구조와 알고리즘을 가능하게 합니다.

typescript
// Generic function: preserves type relationship
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(42);    // T = number, returns number
const s = identity("hi");          // T inferred as string

// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("Alice", 30); // [string, number]

// Generic with array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

제네릭 클래스

제네릭 클래스(<T>)는 타입 안전한 컨테이너를 만듭니다. 각 인스턴스는 특정 타입을 고정합니다 — Stack<number>는 숫자만 받습니다. 이는 런타임 오버헤드 없이 컴파일 타임에 타입 에러를 잡습니다(제네릭은 지워짐). 컬렉션(Stack, Queue, Map)과 반응형 래퍼(Observable<T>)에서 흔합니다.

typescript
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2

const strStack = new Stack<string>();
strStack.push("hello");

제네릭 제약

제약(T extends SomeType)은 제네릭이 받을 수 있는 타입을 제한합니다. 'T extends HasLength'는 T가 'length' 속성을 가짐을 보장합니다. 'K extends keyof T'(keyof 제약)는 객체에 키가 존재함을 보장하며, 올바른 값 타입을 반환합니다. 제약은 제네릭에 대한 타입 안전한 속성 접근과 메서드 호출을 가능하게 합니다.

typescript
// Constraint: T must have a 'length' property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");     // 5 (string has length)
logLength([1, 2, 3]);   // 3 (array has length)
// logLength(42);        // Error: number has no length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a key

기본 타입 매개변수

기본 타입 매개변수는 지정되지 않았을 때 대체 타입을 제공합니다. 일반적인 경우가 있는 API에 유용합니다(예: ApiResponse는 기본값이 string). 기본값은 이전 매개변수에 의존할 수 있습니다. 제약과 결합하여(T extends X = DefaultType) 타입 안전한 선택적 제네릭을 사용하세요.

typescript
// Default type parameter
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

const r1: ApiResponse = { data: "hello", status: 200 };       // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 };    // T = number

// Multiple defaults
class Container<T = string, U = number> {
  constructor(public a: T, public b: U) {}
}

const c1 = new Container("hi", 42);      // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>

// Default with constraint
interface Box<T extends object = { id: number }> {
  value: T;
}

제네릭 인터페이스 & 타입

제네릭 인터페이스와 타입 별칭은 재사용 가능하고 타입 안전한 계약을 만듭니다. Repository<T>는 일관된 API로 데이터 접근을 추상화합니다. Result<T, E>는 예외 없는 에러 처리를 위한 구별된 유니언입니다. 기본 타입 매개변수(E = Error)는 일반적인 경우의 보일러플레이트를 줄입니다.

typescript
// Generic interface
interface Repository<T> {
  findById(id: string): Promise<T>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implement with concrete type
class UserRepo implements Repository<User> {
  async findById(id: string): Promise<User> { /* ... */ }
  async save(user: User): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// Generic type alias with conditional
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };

조건부 타입

조건부 타입(T extends U ? X : Y)은 타입 수준의 if 문입니다. 'infer R'은 다른 타입 내에서 타입을 추출합니다(예: 함수의 반환 타입). 조건부 타입은 유니언에 분배됩니다 — ToArray<string | number>는 string[] | number[]가 됩니다. Exclude, Extract, NonNullable 같은 내장 유틸리티가 이를 사용합니다.

typescript
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

// Extract return type
type AsyncReturnType<T> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;
08

고급 타입

유틸리티 타입

TypeScript는 일반적인 변환을 위한 내장 유틸리티 타입을 제공합니다: Partial(모두 선택적), Pick(키 선택), Omit(키 제외), Record(키-값 맵), Required(선택적 제거), ReturnType(함수 반환), Parameters(함수 매개변수를 튜플로). 이들은 반복적인 타입 정의를 제거합니다.

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

// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };

// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;

// Record: key-value map
type UserMap = Record<string, User>;

// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;

// ReturnType: function return type
type R = ReturnType<() => string>; // string

// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]

템플릿 리터럴 타입

템플릿 리터럴 타입(TS 4.1+)은 다른 타입을 보간하여 문자열 타입을 구축합니다. 유니언과 결합하여 문자열의 데카르트 곱을 생성합니다. Capitalize/Uppercase가 대소문자를 변환합니다. 타입 안전한 이벤트 이름, getter/setter 생성, API 라우트 타이핑에 사용하세요. 매핑된 타입의 'as' 절은 키 이름 변경을 가능하게 합니다.

typescript
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">;    // "Foo"

// Getter names from keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"

infer 키워드

'infer' 키워드는 조건부 타입의 'extends' 절 내에 타입 변수를 선언하여, 재사용을 위해 타입을 캡처합니다. ReturnType, Parameters, Awaited 같은 유틸리티 타입의 기반입니다. infer를 사용하여 복잡한 구조(배열, 프로미스, 함수)에서 타입을 추출하세요 — 수동으로 분해할 필요 없이.

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

type R1 = MyReturnType<() => string>;    // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

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

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number

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

구별된 유니언

구별된 유니언(태그된 유니언)은 공유 리터럴 필드('type', 'kind', 'tag')를 사용하여 변형을 구분합니다. TypeScript는 각 switch 케이스에서 타입을 좁혀 케이스별 필드에 접근하게 합니다. 'never' 기본값은 철저성 검사를 가능하게 합니다 — 새 케이스를 추가하면, 처리할 때까지 컴파일러가 에러를 냅니다. Redux 리듀서와 상태 머신에 필수적입니다.

typescript
// Discriminated union: shared 'type' (or 'kind') field
type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "DELETE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text }];
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      const _: never = action; // exhaustiveness check
      return state;
  }
}

타입 가드: typeof & instanceof

타입 가드는 런타임에 타입을 좁힙니다. 'typeof'는 원시 타입(string, number, boolean, symbol, bigint, undefined, function, object)에 작동합니다. 'instanceof'는 클래스/생성자 프로토타입을 검사합니다. Array.isArray()는 타입화된 배열로 좁힙니다. 이들은 내장 가드입니다 — 커스텀 코드가 필요 없습니다. TypeScript는 각 브랜치에서 좁혀진 타입을 추적합니다.

typescript
// typeof: narrow primitives
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2);      // number
}

// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // Dog
  } else {
    pet.meow(); // Cat
  }
}

// Array.isArray
function flatten(arr: (number | number[])[]) {
  return arr.flatMap(x =>
    Array.isArray(x) ? x : [x]
  );
}

커스텀 타입 가드 & in 연산자

'in' 연산자는 객체에 속성이 존재하는지 검사하여, 그것을 가진 타입으로 좁힙니다. 타입 술어(x is T)는 불리언을 반환하지만 타입도 좁히는 커스텀 가드 함수입니다. 외부 데이터(JSON.parse, API 응답)의 안전한 파싱을 위해 'unknown'을 입력 타입으로 사용하세요. 술어는 재사용 가능하고 조합 가능한 타입 검사를 가능하게 합니다.

typescript
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }

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

// Type predicate: custom guard function
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: any): x is { name: string; age: number } {
  return x && typeof x.name === "string" && typeof x.age === "number";
}

const val: unknown = "hello";
if (isString(val)) {
  console.log(val.toUpperCase()); // narrowed to string
}
09

데이터 구조

배열 & ReadonlyArray

TypeScript 배열은 타입화됩니다 — map, filter, reduce 같은 메서드는 요소 타입을 보존합니다. 불변성을 위해 readonly T[] 또는 ReadonlyArray<T>를 사용하세요. 튜플은 고정 길이와 타입화된 위치를 가집니다. 배열 구조 분해와 스프레드는 완전히 타입 안전합니다. 타입 시스템은 인덱스 범위 초과와 잘못된 타입 할당을 잡습니다.

typescript
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly

// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];

// Array methods are typed
const doubled = nums.map(n => n * 2);    // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number

// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];

객체 & Record

Record<K, V>는 K 타입의 키와 V 타입의 값을 가진 타입화된 맵을 만듭니다. Partial<T>는 모든 속성을 선택적으로 만듭니다 — 업데이트/패치 작업에 이상적입니다. Object.entries/keys/values는 타입화된 배열을 반환합니다. 기존 타입에서 집중된 타입을 파생하려면 Pick과 Omit을 사용하여 타입을 DRY하게 유지하세요.

typescript
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };

// Record: typed key-value map
const scores: Record<string, number> = {
  math: 90,
  science: 85,
};

// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };

// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;

// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores);       // string[]
const values = Object.values(scores);   // number[]

Map & Set

Map과 Set은 완전한 TypeScript 지원을 가진 ES6 컬렉션입니다. Map 키는 모든 타입이 될 수 있습니다(객체와 달리 키를 문자열로 강제하지 않음). Set은 고유한 값을 저장합니다. WeakMap/WeakSet은 키의 가비지 컬렉션을 허용합니다 — DOM 요소나 객체에 메타데이터를 첨부하면서 GC를 방해하지 않을 때 유용합니다.

typescript
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined

// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true

// Iteration (typed)
for (const [key, value] of map) {
  console.log(key, value.name);
}

for (const num of unique) {
  console.log(num);
}

// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();

튜플 & 레이블 튜플

튜플은 타입화된 위치를 가진 고정 길이 배열입니다. 레이블 튜플(TS 4.0+)은 가독성을 위해 이름을 추가합니다 — 반환 값과 CSV 형식 데이터에 유용합니다. 튜플은 인터페이스를 만들지 않고 여러 반환 값을 가능하게 합니다. 변경을 방지하려면 'readonly'를 사용하세요. 튜플은 배열과 다릅니다: [string, number]는 (string | number)[]가 아닙니다.

typescript
// Basic tuple
let point: [number, number] = [10, 20];

// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];

// Destructuring with labels
const [id, name, active] = user;

// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
  return [Math.floor(a / b), a % b];
}

const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2

// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // Error

열거형 & const 열거형

열거형은 명명된 상수를 만듭니다. 문자열 열거형이 권장됩니다(출력에서 읽기 쉽고, 역방향 매핑 문제 없음). const 열거형은 컴파일 타임에 지워집니다(런타임 비용 없음). 간단한 경우에는 유니언 타입('a' | 'b')이 종종 더 낫습니다 — 런타임 코드가 없고, 더 나은 트리 쉐이킹. 그룹화되고 문서화된 상수에는 열거형을 사용하세요.

typescript
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }

// String enum (recommended)
enum HttpStatus {
  OK = "200 OK",
  NotFound = "404 Not Found",
  ServerError = "500 Internal Server Error",
}

// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";

// Exhaustive switch
function handle(s: Status) {
  switch (s) {
    case "pending": return "loading";
    case "success": return "done";
    case "error": return "failed";
  }
}

불변 데이터

TypeScript는 여러 불변성 도구를 제공합니다: 속성을 위한 'readonly', 배열을 위한 ReadonlyArray, Readonly<T> 유틸리티, 리터럴 타입으로 깊은 읽기 전용을 위한 'as const'. 불변 데이터는 우발적 변경을 방지하고 변경 감지를 가능하게 합니다(React, Redux). 불변 업데이트를 위해 스프레드(...)를 사용하세요 — 수정된 필드로 새 객체를 만듭니다.

typescript
// Readonly modifier
interface User {
  readonly id: number;
  name: string;
}

// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error

// Readonly utility type
type FrozenUser = Readonly<User>;

// as const: deep readonly with literal types
const config = {
  endpoint: "/api",
  methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]

// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new object
10

모듈 & 네임스페이스

ES 모듈: Import & Export

TypeScript는 ES 모듈 구문(import/export)을 사용합니다. 명명된 내보내기는 명시적입니다; 기본 내보내기는 단일 '주요' 내보내기입니다. 네임스페이스 임포트를 위해 'import * as'를 사용하세요. 모듈 해석은 Node.js 규칙을 따릅니다(node_modules, 확장자). tsconfig.json에서 'module'과 'moduleResolution'을 설정하세요.

typescript
// math.ts - exporting
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a: number, b: number): number {
  return a * b;
}

// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import

console.log(add(1, 2));      // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI);        // 3.14159

타입 전용 임포트

'import type'은 타입만 임포트합니다(컴파일 타임에 지워짐, 런타임 코드 없음). 이는 순환 종속성과 불필요한 런타임 임포트를 방지합니다. TS 4.5+은 혼합 임포트에서 인라인 'type' 수정자를 허용합니다. 번들 크기를 줄이기 위해 인터페이스, 타입 별칭, 열거형(const인 경우)에 타입 전용 임포트를 사용하세요.

typescript
// Type-only import (erased at runtime)
import type { User, Config } from "./types";

// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";

// Re-export types
export type { User } from "./types";

// Import type for interfaces and types
interface UserService {
  get(id: string): User;  // User from type-only import
}

// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoid

동적 임포트

동적 임포트(import())는 모듈을 주문형으로 로드하여 Promise를 반환합니다. 이는 코드 분할과 지연 로딩을 가능하게 합니다 — 웹 앱 성능에 중요합니다. TypeScript는 모듈 타입을 자동으로 추론합니다. 선택적 기능, 대형 라이브러리, 라우트 기반 코드 분할(React.lazy, Next.js dynamic)에 사용하세요.

typescript
// Dynamic import returns a Promise
async function loadModule() {
  const module = await import("./heavy-module");
  module.doWork();
}

// Conditional loading
if (featureEnabled) {
  const { Feature } = await import("./feature");
  new Feature().init();
}

// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");

// With error handling
try {
  const lib = await import("./optional-lib");
  lib.run();
} catch (e) {
  console.log("Library not available");
}

선언 파일 & 모듈 보강

선언 파일(.d.ts)은 JS 모듈, CSS/PNG 임포트, 전역 변수에 대한 타입을 설명합니다. 모듈 보강은 기존 모듈 타입을 확장합니다 — Express Request, Express Response, 또는 서드파티 타입에 속성을 추가하는 데 유용합니다. 이것이 passport 같은 미들웨어가 req.user 타이핑을 추가하는 방법입니다.

typescript
// global.d.ts - declare ambient modules
declare module "*.css" {
  const content: string;
  export default content;
}

declare module "*.png" {
  const src: string;
  export default src;
}

// Module augmentation - extend existing types
import express from "express";

declare module "express" {
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now req.user is typed
app.get("/", (req, res) => {
  console.log(req.user?.name);
});

네임스페이스 (레거시)

네임스페이스는 TypeScript의 ES6 이전 모듈 시스템입니다. 관련 코드를 명명된 객체 아래로 그룹화합니다. 새 프로젝트에는 ES 모듈(import/export)을 선호하세요 — 표준화되고, 트리 쉐이킹 가능하며, 번들러와 작동합니다. 네임스페이스는 전역 타입 선언과 레거시 코드를 위한 .d.ts 선언 파일에서 여전히 유용합니다.

typescript
// Namespace: pre-ES6 way to organize code
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true

// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarations

tsconfig 모듈 설정

tsconfig 모듈 설정은 TypeScript가 임포트를 처리하는 방법을 제어합니다. 'moduleResolution: node'는 Node.js 해석을 사용합니다(node_modules 조회). 'esModuleInterop'은 CommonJS에서 기본 임포트를 활성화합니다. 'paths'는 더 깔끔한 임포트를 위한 임포트 별칭(@/components)을 만듭니다. 'resolveJsonModule'은 추론된 타입으로 .json 파일 임포트를 허용합니다.

typescript
{
  "compilerOptions": {
    "module": "ESNext",           // ES module output
    "moduleResolution": "node",   // Node-style resolution
    "esModuleInterop": true,      // allow default imports from CJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // import .json files
    "isolatedModules": true,      // each file is independent
    "baseUrl": "./src",           // base for non-relative imports
    "paths": {
      "@/*": ["./*"],             // path alias
      "@components/*": ["./components/*"]
    }
  }
}

// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"
11

비동기 & 프로미스

Promise 타입

Promise<T>는 핵심 비동기 타입입니다 — T는 해결된 값 타입입니다. TypeScript는 .then() 체인을 통해 타입을 추론합니다. 콜백 기반 API를 래핑하려면 'new Promise()'를 사용하세요. 항상 resolve/reject 값을 타입화하세요. 가독성과 에러 처리를 위해 원시 .then() 체인보다 async/await를 선호하세요.

typescript
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
  // user is User, not any
  console.log(user.name); // string
});

async/await

async/await는 Promise 위의 구문적 설탕입니다 — 'await'는 Promise가 해결될 때까지 일시 정지합니다. async 함수는 항상 Promise를 반환합니다. 병렬 실행을 위해 Promise.all()을 사용하세요(순차적 await보다 훨씬 빠름). 최상위 await는 ES2022+에서 ES 모듈에서 작동합니다. TypeScript는 await된 값이 Promise인지 검사합니다.

typescript
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);

// Sequential vs parallel
async function loadAll() {
  // Sequential (slow)
  const a = await getUser(1);
  const b = await getUser(2);

  // Parallel (fast)
  const [x, y] = await Promise.all([getUser(1), getUser(2)]);
  return { a, b, x, y };
}

비동기에서의 에러 처리

async/await에서 에러 처리를 위해 try/catch를 사용하세요 — .catch()보다 깔끔합니다. TypeScript는 타입화된 throw를 지원하지 않으므로(catch에서 모든 에러는 unknown), instanceof로 좁히세요. 예측 가능한 에러의 경우, 예외 대신 Result 타입 패턴(ok/error 유니언)을 고려하세요 — 타입 시그니처에서 에러 처리를 명시적으로 만듭니다.

typescript
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
  try {
    const data = await fetch("/api/data");
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.text();
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}

// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

// Result type as alternative to exceptions
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function safeFetch(url: string): Promise<Result<string>> {
  try {
    const res = await fetch(url);
    return { ok: true, value: await res.text() };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

Promise 컴비네이터

Promise 컴비네이터는 여러 비동기 작업을 조정합니다: all()(병렬, 실패 시 즉시 중단), allSettled()(병렬, 모두 대기), race()(첫 번째 완료), any()(첫 번째 성공). 의존 데이터 로딩에는 all()을, 부분 결과를 원할 때 allSettled()를, 타임아웃에는 race()를, 중복 페치에는 any()를 사용하세요.

typescript
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
  fetch("/api/a"),
  fetch("/api/b"),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow"),
]);

// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
  fetch("/api/primary"),
  fetch("/api/fallback"),
]);

이벤트 루프 & 마이크로태스크

JavaScript의 이벤트 루프는 마이크로태스크(Promise 콜백, queueMicrotask)를 매크로태스크(setTimeout, setInterval)보다 먼저 처리합니다. 이것이 Promise가 타임아웃보다 먼저 해결되는 이유입니다. 비동기 반복(for await...of)은 비동기 이터러블을 소비합니다 — 스트림에 유용. 비동기 제너레이터(async function*)는 비동기 이터러블을 생성하여 지연 비동기 시퀀스를 가능하게 합니다.

typescript
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
// Output: 1, 2, 3, 4

// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));

// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
  for await (const chunk of stream) {
    console.log(chunk.length);
  }
}

// Async generators
async function* generate() {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

동시성 패턴

이 패턴들은 TypeScript에서 완전히 타입 안전합니다. 디바운스는 호출이 N ms 동안 멈출 때까지 실행을 지연합니다(검색 입력). 스로틀은 N ms당 한 번의 호출로 제한합니다(스크롤 핸들러). 세마포어/mapLimit은 동시성을 제어합니다 — 속도 제한된 API에 유용. Parameters<T>와 ReturnType<T>는 래퍼에서 함수 시그니처를 보존합니다.

typescript
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Semaphore (limit concurrency)
async function mapLimit<T, U>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<U>
): Promise<U[]> {
  const results: U[] = [];
  const executing: Promise<void>[] = [];
  for (const item of items) {
    const p = fn(item).then(r => results.push(r));
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}
12

에러 처리 & 테스팅

try/catch와 unknown

TypeScript 4.4(useUnknownInCatchVariables)부터, catch된 에러는 'unknown'입니다 — 사용하기 전에 반드시 좁혀야 합니다. 이는 존재하지 않는 속성 접근을 방지합니다. 특정 에러 타입을 검사하려면 instanceof를, 또는 대체로 String()을 사용하세요. 일관된 에러 추출을 위해 getErrorMessage() 헬퍼를 만드세요.

typescript
// In strict mode, catch is 'unknown' (not 'any')
try {
  JSON.parse("invalid");
} catch (error: unknown) {
  // Must narrow before using
  if (error instanceof SyntaxError) {
    console.error("JSON error:", error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

// Helper function
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

커스텀 에러 클래스

커스텀 에러 클래스는 에러에 구조화된 데이터(code, statusCode, field)를 추가합니다. 항상 super(message)를 호출하고 프로토타입을 설정(Object.setPrototypeOf)하여 TypeScript/ES5 프로토타입 체인 문제를 수정하세요. catch 블록에서 에러 타입을 구별하려면 instanceof를 사용하세요. 이 패턴은 Express 미들웨어와 API 에러 핸들러에 필수적입니다.

typescript
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "AppError";
    // Fix prototype chain (TS quirk)
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public field: string) {
    super(message, "VALIDATION_ERROR", 400);
    this.name = "ValidationError";
  }
}

// Usage
function createUser(input: unknown) {
  if (typeof input !== "object" || input === null) {
    throw new ValidationError("Invalid input", "body");
  }
}

try {
  createUser("bad");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.statusCode); // "body" 400
  }
}

Result 타입 패턴

Result 타입(Rust에서 유래)은 타입 시그니처에서 에러를 명시적으로 만듭니다 — 호출자가 성공과 실패 모두를 처리해야 합니다. 예외와 달리 컴파일러가 에러 처리를 강제합니다. 예외가 과한 예상 실패(검증, 찾을 수 없음)에 사용하세요. 진정으로 예기치 않은 에러(버그, 시스템 실패)에는 예외를 예약하세요.

typescript
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
  console.log(result.value); // number
} else {
  console.error(result.error); // string
}

// Utility helpers
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

어설션 함수

어설션 함수(asserts X)는 조건이 실패하면 예외를 던지고 이후 타입을 좁힙니다. 'asserts value is string'은 호출 후 value가 string임을 TypeScript에 알립니다. 이는 반복된 if 검사보다 깔끔합니다. 경계(API 입력, 설정)에서 런타임 검증에 사용하세요. 스키마 검증을 위해 Zod나 io-ts와 결합하세요.

typescript
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string

// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) throw new Error("undefined");
}

타입 안전한 JSON 파싱

JSON.parse는 'any'를 반환합니다 — 안전하지 않습니다. 런타임에 형태를 검증하고 타입을 좁히기 위해 타입 가드로 래핑하세요. 복잡한 스키마의 경우, Zod, io-ts, 또는 yup을 사용하세요 — 단일 스키마 정의에서 런타임 검증자와 TypeScript 타입을 모두 생성합니다. 이는 API 응답과 사용자 입력에 필수적입니다.

typescript
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) return parsed;
    return null;
  } catch {
    return null;
  }
}

// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && typeof (x as User).id === "number"
    && typeof (x as User).name === "string";
}

const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
  console.log(data.name); // User
}

// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));

철저성 검사

철저성 검사는 유니언의 모든 케이스를 처리함을 보장합니다. 기본 케이스를 'never'에 할당하세요 — 유니언에 새 변형을 추가하면, 새 타입이 'never'에 할당할 수 없어 TypeScript가 에러를 냅니다. 이는 컴파일 타임에 누락된 케이스를 잡습니다. 구별된 유니언, Redux 리듀서, 상태 머신에 필수적입니다.

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.w * shape.h;
    default:
      // If a new shape is added, this errors
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'
13

데코레이터 & 메타데이터

클래스 데코레이터

클래스 데코레이터는 생성자 함수를 받고 수정된 클래스를 반환할 수 있습니다. 데코레이터 팩토리(함수를 반환하는)는 인수를 받습니다. 데코레이터는 실험적 기능입니다 — tsconfig에서 'experimentalDecorators'를 활성화하세요. NestJS, TypeORM, Angular에서 의존성 주입과 메타데이터에 많이 사용됩니다.

typescript
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Created ${target.name}`);
    }
  };
}

@Logged
class Service {
  constructor(public name: string) {}
}

const s = new Service("Auth");
// Logs: "Created Service"

// Decorator factory (with arguments)
function Prefix(prefix: string) {
  return function <T extends new (...args: any[]) => any>(target: T): T {
    return class extends target {
      message = prefix + " " + (this as any).name;
    };
  };
}

메서드 & 속성 데코레이터

메서드 데코레이터는 (target, propertyKey, descriptor)를 받고 원래 메서드를 래핑할 수 있습니다 — 로깅, 캐싱, 접근 제어에 유용. 속성 데코레이터는 (target, key)를 받고 메타데이터 등록에 자주 사용됩니다. descriptor.value는 원래 함수입니다; 동작을 추가하려면 래핑하세요. NestJS(@Get, @Post)와 TypeORM(@Column)에서 흔합니다.

typescript
// Method decorator: (target, key, descriptor)
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}(${args})`);
    return original.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"

// Property decorator: (target, key)
function Required(target: any, key: string) {
  // Store metadata for validation
  console.log(`Required: ${key}`);
}

매개변수 데코레이터 & 메타데이터

매개변수 데코레이터는 (target, key, index)를 받고 의존성 주입에 사용됩니다(NestJS, Angular). 'reflect-metadata' 폴리필은 런타임 타입 메타데이터를 가능하게 합니다 — 데코레이터는 Reflect.getMetadata('design:paramtypes')로 매개변수 타입에 접근할 수 있습니다. 이것이 DI 컨테이너가 무엇을 주입할지 아는 방법입니다. tsconfig에서 'emitDecoratorMetadata: true'로 활성화하세요.

typescript
import "reflect-metadata";

// Parameter decorator
function Inject(target: any, key: string, index: number) {
  console.log(`Inject param ${index} of ${key}`);
}

class Service {
  constructor(@Inject private dep: any) {}
}

// Store and retrieve metadata
const METADATA_KEY = "design:type";

class Example {
  greet(name: string): void {}
}

// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)

const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]

접근자 데코레이터

접근자 데코레이터는 getter/setter에 적용됩니다. descriptor에는 래핑할 get/set 속성이 있습니다. 검증, 로깅, 또는 열거 가능성 변경에 사용하세요. 검증 패턴(MaxLength, Min, Max)은 setter를 래핑하여 런타임에 제약을 강제합니다. 이것이 class-validator(NestJS)가 DTO 검증에 작동하는 방식입니다.

typescript
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
  return function (
    target: any,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string = "";

  @Enumerable(true)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)

// Validation decorator
function MaxLength(len: number) {
  return function (target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length > len) throw new Error("Too long");
        value = v;
      },
    });
  };
}

현대 데코레이터 (TC39 Stage 3)

TypeScript 5.0은 TC39 Stage 3 데코레이터 제안을 지원합니다 — experimentalDecorators를 대체하는 표준화된 API입니다. 새 API는 (target, key, descriptor) 대신 컨텍스트 객체(ClassMethodDecoratorContext)를 사용합니다. 더 깔끔하고, 타입 안전하며, 결국 JS 표준에 들어올 것입니다. 새 프로젝트에 사용하세요; 실험적 데코레이터는 NestJS/Angular 호환성을 위해 남아 있습니다.

typescript
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class Service {
  @log
  greet(name: string): string {
    return `Hello, ${name}`;
  }
}

// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after construction

실용적 데코레이터: 메모이제이션

이 메모이제이션 데코레이터는 인수를 기반으로 메서드 결과를 캐시합니다 — fibonacci 같은 비싼 순수 함수에 극적인 속도 향상. 캐시는 인스턴스별입니다(공유 캐시를 위해 WeakMap 사용). 데코레이터는 횡단 관심사에 빛을 발합니다: 로깅, 캐싱, 검증, 접근 제어, 재시도 로직. 인프라 관심사를 분리하여 비즈니스 로직을 깔끔하게 유지합니다.

typescript
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, Return>();
  return function (this: This, ...args: Args): Return {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n < 2) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");

console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");
14

유틸리티 타입

Partial, Required & Readonly

Partial<T>는 모든 속성을 선택적으로 만듭니다 — 일부 필드만 변경되는 업데이트/패치 작업에 완벽합니다. Required<T>는 역입니다. Readonly<T>는 모든 속성을 컴파일 타임에 불변으로 만듭니다. 이들은 가장 일반적으로 사용되는 유틸리티 타입이며, 병렬 optional/readonly 인터페이스를 수동으로 유지할 필요를 없앱니다.

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

// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

function updateUser(id: number, changes: UserUpdate) {
  // Only provided fields are updated
  Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name

// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required

// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly

// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutation

Pick, Omit & Record

Pick<T, K>는 속성의 부분 집합을 추출합니다; Omit<T, K>는 속성을 제거합니다 — 둘 다 중복 없이 파생된 타입을 만듭니다. Record<K, V>는 특정 키를 가진 맵/딕셔너리 타입을 만듭니다. 이들은 DTO(데이터 전송 객체)에 필수적입니다: id와 createdAt 같은 자동 생성 필드를 생략하여 User에서 CreateUser 타입을 파생하세요. 이는 타입을 DRY하고 동기화 상태로 유지합니다.

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }

// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;

ReturnType, Parameters & Awaited

ReturnType과 Parameters는 기존 함수에서 타입을 추출합니다 — 시그니처를 복제하고 싶지 않은 함수를 래핑하거나 호출할 때 귀중합니다. Awaited<T>는 중첩된 Promise를 풉니다(Promise<Promise<T>>가 T가 됨), 비동기 함수 반환 타입에 필수적입니다. InstanceType은 클래스 생성자에서 인스턴스 타입을 얻습니다. 이들은 타입 안전한 함수 컴포지션과 고차 유틸리티를 가능하게 합니다.

typescript
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>

// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }

// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number

// ConstructorParameters<T>: parameters of a class constructor
class Point {
  constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]

// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // Point

Exclude, Extract & NonNullable

Exclude<T, U>는 유니언에서 타입을 제거합니다; Extract<T, U>는 일치하는 타입만 유지합니다 — 둘 다 유니언 멤버에 작동합니다. NonNullable<T>는 null과 undefined를 제거합니다. 이들은 빌딩 블록입니다: Omit은 Pick<T, Exclude<keyof T, K>>로 정의됩니다. 유니언 타입을 동적으로 필터링하려면 Exclude/Extract를 사용하세요, 예를 들어 result 유니언에서 성공 타입과 에러 타입을 분리할 때.

typescript
type Role = "admin" | "user" | "guest" | "superadmin";

// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"

// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"

// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Practical: filter union types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"

// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

커스텀 유틸리티 타입

커스텀 유틸리티 타입은 특정 요구를 위해 내장 타입을 조합합니다. Optional<T, K>는 특정 필드만 선택적으로 만듭니다(Partial보다 더 목적적). DeepPartial/DeepReadonly는 중첩된 객체에 재귀적으로 적용됩니다 — 설정과 상태 트리에 유용. Mutable의 -readonly 수정자는 readonly를 제거합니다. 이 패턴들은 매핑된 타입과 조건부 타입이 어떻게 결합하여 강력한 타입 수준 프로그래밍을 가능하게 하는지 보여줍니다.

typescript
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }

// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Mutable (remove readonly from all properties)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
  [K in keyof T]: T[K] extends never ? never : K;
}[keyof T];
15

조건부 타입

기본 조건부 타입 (T extends U ? X : Y)

조건부 타입(T extends U ? X : Y)은 타입 수준 조건에 따라 타입을 선택합니다 — 타입을 위한 삼항 연산자 같습니다. TypeScript 타입 수준 프로그래밍의 기반입니다. T가 유니언일 때, 조건은 각 멤버에 분배됩니다(분배적 조건부 타입). infer 키워드는 패턴 내에서 타입을 추출합니다, 배열에서 요소 타입이나 Promise에서 해결 타입을 꺼내는 것처럼.

typescript
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y

// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (not an array)

// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<number>>;  // number
type E = UnwrapPromise<string>;           // string (not a promise)

// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<number>;      // false

// Conditional types are evaluated lazily and distribute over unions

infer 키워드 (타입 추출)

infer 키워드는 조건부 타입의 extends 절 내에 타입 변수를 선언하여, 해당 위치에 일치하는 타입을 캡처합니다. ReturnType, Parameters, Awaited가 구현되는 방식입니다. infer는 재귀적으로 사용될 수 있어(Unwrap<Promise<Promise<T>>>) 중첩된 타입을 완전히 풉니다. 복잡한 제네릭 구조에서 타입을 추출하는 주요 도구입니다.

typescript
// infer extracts a type from within a pattern

// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)

// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number

// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;

// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;

분배적 조건부 타입

조건부 타입은 유니언에 분배됩니다: ToArray<A | B>를 적용하면 ToArray<A> | ToArray<B>가 됩니다, (A | B)[]가 아닙니다. 이것이 Exclude와 NonNullable이 유니언 멤버를 필터링하는 방식입니다 — 제외된 타입에 대해 'never'를 반환하며, 이는 유니언에서 붕괴됩니다. 분배를 방지하려면 양쪽을 대괄호로 감싸세요: [T] extends [U]. 분배는 필터링에 보통 원하는 것이지만, '전체 유니언 래핑' 작업에는 비분배가 필요합니다.

typescript
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y  applied to  A | B  becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]

// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]  — a single array of the union

// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number  (null and undefined filtered out)

// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;

조건부 타입 제약

조건부 타입은 중첩되어 타입 수준 판별(타입을 위한 switch 문 같은)을 만들 수 있습니다. infer와 결합하여, 제네릭 매개변수에서 타입을 추출하고 파생합니다. 이것이 React 같은 라이브러리가 컴포넌트 정의에서 prop 타입을 파생하는 방법이며, 라우팅 라이브러리가 경로 문자열에서 매개변수 타입을 추출하는 방식입니다. 제약(T extends any[])은 조건부가 요소 타입을 추출하기 전에 입력이 유효함을 보장합니다.

typescript
// Use conditional types to constrain and derive types

// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type

// Conditional with multiple branches (nested)
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>;         // "object"

// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
  return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // number

템플릿 리터럴 타입 (문자열 조작)

템플릿 리터럴 타입은 타입 수준 문자열 조작을 가능하게 합니다 — 연결, 대소문자 변환, 패턴 매칭. 조건부 타입과 infer와 결합하여, 경로 문자열을 파싱하여 라우트 매개변수를 추출하고, 이벤트 핸들러 이름을 생성하거나, 타입 안전한 속성 접근자를 구축할 수 있습니다. 이것이 Next.js와 tRPC 같은 프레임워크가 문자열 리터럴에서 엔드투엔드 타입 안전 API를 만드는 방식입니다.

typescript
// Template literal types: type-level string operations

type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">;    // "Foo"

// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Extract route parameters
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}/${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}/${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"

// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T ? Get<T[Key], Rest> : never
    : P extends keyof T ? T[P] : never;
16

매핑된 타입

기본 매핑된 타입

매핑된 타입은 객체의 키를 순회하며 각 속성을 변환합니다 — [K in keyof T]가 구문입니다. Partial, Readonly, Pick과 다른 유틸리티 타입이 구현되는 방식입니다. 속성 타입을 수정하거나(T[K] | null), 수정자(? 또는 readonly)를 추가하거나, 값 타입을 완전히 교체할 수 있습니다. 매핑된 타입은 TypeScript 타입 변환 시스템의 중추입니다.

typescript
// Mapped types transform each property of an existing type

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

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

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

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

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

// Change all property types to a specific type
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }

'as'를 통한 키 리매핑

키 리매핑(as 절, TS 4.1+)은 매핑 중에 키 이름을 바꾸거나 필터링할 수 있게 합니다. 템플릿 리터럴 타입을 사용하여 키 이름을 변환하세요(접두사 추가, getter로 변환, 대문자화). 키에 대해 'never'를 반환하면 제거됩니다 — 이것이 속성을 필터링하는 방법입니다. 조건부 타입과 결합하여, 데이터 스키마를 검증 스키마로 변환하거나 API 타입을 폼 타입으로 변환하는 강력한 변환이 가능합니다.

typescript
// Remap keys using 'as' (TypeScript 4.1+)

// Add a prefix to all keys
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  id: number;
  name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }

// Convert keys to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Filter out keys by returning never
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }

// Make keys UPPERCASE
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

수정자: +, -, ?, readonly

+ 와 - 수정자는 속성 수정자를 추가하거나 제거합니다. -?는 선택성을 제거합니다(선택적 필드를 필수로); -readonly는 불변성을 제거합니다. 이것이 Required<T>와 Mutable 패턴이 작동하는 방식입니다. + 접두사는 선택적입니다(readonly는 +readonly와 동일), 하지만 제거를 위해서는 - 가 필요합니다. 이는 타입 변환 중에 속성 특성에 대한 세밀한 제어를 제공합니다.

typescript
// Add (+) or remove (-) modifiers

// Remove optional (?) modifier: -?
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface OptionalUser {
  id?: number;
  name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now

// Remove readonly modifier: -readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface FrozenConfig {
  readonly port: number;
  readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now

// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
  +readonly [K in keyof T]: T[K];
};

// Add optional modifier: +?
type MakeOptional<T> = {
  [K in keyof T]+?: T[K];
};

동형 매핑된 타입

동형 매핑된 타입([K in keyof T])은 소스 타입에서 속성 수정자(readonly, ?)를 보존합니다 — 이것이 Pick<User, 'id'>가 id를 readonly로 유지하는 이유입니다. 비동형 매핑(예: [K in string])은 수정자를 보존하지 않습니다. 이는 타입을 파생할 때 중요합니다: readonly 필드를 가진 타입의 동형 Partial은 그 필드를 readonly로 유지합니다(하지만 선택적). 동형성 이해는 수정자가 변환 후에도 살아남는지 예측하는 데 도움이 됩니다.

typescript
// Homomorphic mapped types preserve modifiers from the original type

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

// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
  [K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User

// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
  [K in string]: T; // not keyof T — loses modifiers
};

// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!

// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
  [K in keyof T]?: T[K]; // Partial is homomorphic
};

스키마에서 검증 타입 구축

이것이 폼 라이브러리(React Hook Form, Formik)와 검증 라이브러리(Zod, Yup)가 타입 안전성을 유지하는 방식입니다 — 매핑된 타입을 사용하여 데이터 인터페이스에서 검증자와 폼 타입을 파생합니다. User에 필드를 추가하면, 검증자와 폼 타입도 자동으로 그것을 요구하여 드리프트를 방지합니다. 이는 매핑된 타입의 실제 강력함을 보여줍니다: 하나의 진실 소스(인터페이스)가 여러 파생 타입을 이끕니다.

typescript
// Practical: derive a validator type from a data type

// Given a data interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Create a validator type where each field is a validation function
type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean;
};

const userValidator: Validator<User> = {
  id: (v) => v > 0,
  name: (v) => v.length > 0,
  email: (v) => v.includes("@"),
};

// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type Form<T> = {
  [K in keyof T]: FormField<T[K]>;
};

const userForm: Form<User> = {
  id: { value: 1, error: null, touched: false },
  name: { value: "Alice", error: null, touched: true },
  email: { value: "", error: "Required", touched: true },
};

// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.
17

타입 가드 & 좁히기

typeof & instanceof 좁히기

TypeScript는 런타임 검사를 기반으로 타입을 좁힙니다. typeof는 원시 타입(string, number, boolean 등)을 좁힙니다; instanceof는 클래스 인스턴스를 좁힙니다. 진실성 검사(if (value))는 null/undefined/0/''/false를 좁힙니다. 좁히기는 조건이 유지되는 브랜치 내에 적용됩니다. 이것이 TypeScript가 런타임 검사에 타입 정보를 실어, 명시적 캐스트의 필요를 없애는 방식입니다.

typescript
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
    // padding is narrowed to 'number' here
  }
  return padding + value;
  // padding is narrowed to 'string' here
}

// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow(); // OK — narrowed to Cat
  } else {
    animal.bark(); // OK — narrowed to Dog
  }
}

// typeof returns: "string" | "number" | "boolean" | "symbol"
//   "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)

// Truthiness narrowing
function process(value?: string) {
  if (value) {
    console.log(value.toUpperCase()); // value is string (not undefined)
  }
}

in 연산자 & 구별된 유니언

'in' 연산자는 속성 존재 여부로 좁힙니다. 구별된 유니언은 공유 리터럴 속성('kind' 또는 'type' 같은)을 태그로 사용합니다 — 이를 switch하면 모든 속성 접근과 함께 올바른 변형으로 좁힙니다. 이는 TypeScript의 합 타입 / 대수적 데이터 타입에 해당합니다. Redux 액션, 상태 머신, 여러 형태의 API 응답에 대한 표준 패턴입니다. 판별자에는 항상 리터럴 타입을 사용하세요.

typescript
// 'in' operator checks for a property — narrows to types that have it

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // narrowed to Fish
  } else {
    animal.fly(); // narrowed to Bird
  }
}

// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed by kind
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)

커스텀 타입 가드 함수

커스텀 타입 가드(반환 타입 'x is Type')는 복잡한 런타임 검사를 타입을 좁히는 재사용 가능한 함수로 캡슐화할 수 있게 합니다. 어설션 함수(asserts x is T)는 불리언을 반환하는 대신 예외를 던집니다 — 호출 후 모든 코드에서 좁힙니다. 타입 가드를 사용하여 신뢰할 수 없는 데이터(JSON.parse, API 응답)를 검증하고 타입 시스템으로 가져오세요. 이것이 런타임 검증과 컴파일 타임 타입 사이의 간극을 연결합니다.

typescript
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true

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

function isUser(obj: any): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');

if (isUser(data)) {
  console.log(data.name); // data is narrowed to User
}

// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
  return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}

// Type guard for discriminated unions
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
  return r.success;
}

// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Expected value to be defined");
  }
}

never로 철저성 검사

철저성 검사는 'never' 타입을 사용하여 모든 유니언 변형이 처리됨을 보장합니다. 유니언에 새 변형을 추가했지만 케이스를 잊으면, 기본 브랜치의 'never' 할당이 컴파일 에러가 됩니다. assertNever 헬퍼는 런타임에 예외를 던지고 컴파일 타임에 누락된 케이스에 대해 에러를 냅니다. 이는 구별된 유니언에 가장 가치 있는 패턴입니다 — 케이스를 잊었을 때 컴파일러가 알려줍니다.

typescript
// The 'never' type ensures all cases are handled

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If we add a new shape but forget a case, this line
      // becomes a compile error because 'shape' would not be 'never'
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'

// Helper function pattern
function assertNever(x: never): never {
  throw new Error("Unexpected: " + JSON.stringify(x));
}

function process(shape: Shape) {
  switch (shape.kind) {
    case "circle": return;
    case "square": return;
    default: return assertNever(shape); // compile error if a case is missing
  }
}

배열 메서드로 좁히기

Array.filter는 콜백이 타입 가드가 아닌 불리언을 반환하므로 기본적으로 요소 타입을 좁히지 않습니다. 좁히려면 커스텀 타입 가드 함수(pet is Dog)를 전달하세요 — 그러면 filter는 좁혀진 배열 타입을 반환합니다. TypeScript는 if 검사를 기반으로 콜백 본문 내에서도 좁힙니다(forEach, map). Array.isArray는 unknown/any를 배열 타입으로 좁히는 내장 타입 가드입니다.

typescript
// TypeScript narrows through filter, map, and control flow

interface Pet {
  name: string;
  speak(): void;
}

class Dog implements Pet {
  name = "Rex";
  speak() { console.log("Woof"); }
  fetch() { console.log("Fetching!"); }
}

class Cat implements Pet {
  name = "Whiskers";
  speak() { console.log("Meow"); }
}

const pets: Pet[] = [new Dog(), new Cat(), new Dog()];

// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
  return pet instanceof Dog;
}

const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog

// Narrowing in forEach/callbacks
pets.forEach((pet) => {
  if (isDog(pet)) {
    pet.fetch(); // narrowed to Dog inside callback
  }
});

// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
  if (Array.isArray(input)) {
    input.length; // OK — input is any[] (or unknown[])
  }
}
18

타입 추론

변수 & 반환 타입 추론

TypeScript는 초기화와 반환문에서 타입을 추론하므로, 명시적 어노테이션이 거의 필요 없습니다. 변수는 일반 타입으로 넓힙니다(let x = 10은 10이 아닌 number 추론). 'as const'는 넓힘을 방지합니다: 리터럴이 리터럴로, 객체를 readonly로, 배열을 readonly 튜플로 만듭니다. typeof colors[number]는 튜플 요소 타입의 유니언을 추출합니다 — 배열에서 열거형 같은 타입을 파생하는 일반적인 패턴입니다.

typescript
// TypeScript infers types when you don't annotate

// Variable inference
let count = 0;          // number
let name = "Alice";     // string
let items = [1, 2, 3];  // number[]
let mixed = [1, "two"]; // (string | number)[]

// Function return type inference
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// const assertions for literal types
const x = 10;        // number (widened)
const y = "hello";   // string (widened)
const z = { a: 1 };  // { a: number }

const x2 = 10 as const;       // 10 (literal type)
const y2 = "hello" as const;  // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }

// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"

문맥적 타이핑

문맥적 타이핑은 기대 타입을 표현식으로 역방향으로 흘려보냅니다. 함수를 타입화된 변수에 할당하면, 매개변수 타입이 대상 타입에서 추론됩니다. 이것이 이벤트 핸들러, 배열 콜백, 객체 리터럴이 종종 타입 어노테이션이 필요 없는 이유입니다. 경험칙: 함수 시그니처(공개 API의 매개변수와 반환 타입)는 어노테이션하고, 지역 변수와 콜백은 추론에 맡기세요.

typescript
// Contextual typing: the expected type influences the inferred type

// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
  // mouseEvent is inferred as MouseEvent (from onmousedown's type)
  console.log(mouseEvent.button);
};

// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]

// Contextual typing for object literals
interface User {
  name: string;
  age: number;
}
const user: User = {
  name: "Alice", // inferred as string (from User.name)
  age: 30,
};

// Contextual typing with discriminated unions
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; scrollTop: number };

function handle(e: Event) {
  if (e.type === "click") {
    console.log(e.x, e.y); // narrowed
  }
}

// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation needed

최적 공통 타입 (유니언 추론)

여러 값에서 추론할 때(배열 리터럴처럼), TypeScript는 '최적 공통 타입'을 찾습니다 — 보통 슈퍼타입 또는 유니언입니다. [Dog, Cat] 배열은 (Dog | Cat)[]가 아닌 Animal[](공통 기반)로 추론됩니다. 유니언을 얻으려면 명시적으로 어노테이션하세요. 조건부 반환은 모든 브랜치의 유니언을 추론합니다. 이를 이해하면 명시적 어노테이션이 필요한 때와 추론이 충분한 때를 예측할 수 있습니다.

typescript
// When inferring from multiple values, TS finds the best common type

// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]

// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]

// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }

const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])

// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];

// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]

// Return type inference with conditionals
function getValue(flag: boolean) {
  return flag ? 42 : "hello"; // inferred as number | string
}

제어 흐름 분석

TypeScript는 제어 흐름 분석을 수행합니다 — if/else, 반환, 할당, 논리 연산자를 통해 타입이 어떻게 좁혀지고 넓혀지는지 추적합니다. 타입은 검사 후 좁혀지고 변수가 재할당될 때까지 좁혀진 상태로 유지됩니다. 조기 반환(가드 절)은 특히 효과적입니다: 'if (value === null) return' 후, 함수의 나머지는 value가 null이 아님을 압니다. 이것이 가드 절 스타일 코드가 TypeScript와 잘 작동하는 이유입니다.

typescript
// TypeScript tracks types through control flow

function example(value: string | number | null) {
  // After this check, 'value' is string | number in this branch
  if (value === null) {
    return;
  }

  // value is now string | number (null narrowed out)
  console.log(value);

  if (typeof value === "string") {
    return value.toUpperCase(); // value is string
  }

  // value is now number (string and null narrowed out)
  return value.toFixed(2);
}

// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number

// Narrowing with && and ||
function process(input?: string) {
  const value = input && input.trim(); // string | undefined
  const safe = input || "default"; // string (undefined narrowed out)
}

// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCase

satisfies 연산자 (TS 4.9+)

'satisfies' 연산자(TS 4.9+)는 값을 타입에 맞는지 검증하면서 가장 구체적인 추론 타입을 보존합니다 — 넓히는 타입 어노테이션과 달리. 이는 설정, 라우트 맵, 테마 객체에 이상적입니다: 구조가 올바름을 컴파일 타임에 검증하면서, 속성 접근은 여전히 정밀한 리터럴 타입을 반환합니다. 리터럴 보존과 구조적 검증 모두를 위해 'as const'과 결합하세요.

typescript
// 'satisfies' checks a value matches a type WITHOUT widening it

// Problem: annotation widens the type
const config1: Record<string, string | number> = {
  port: 8080,
  host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)

// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
  port: 8080,
  host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated

// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;

const routes = {
  "/users": { method: "GET", handler: () => fetchUsers() },
  "/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;

routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)

// 'as const' + 'satisfies': literal types + validation
const colors = {
  primary: "#ff0000",
  secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;
19

선언 파일 & 모듈 보강

.d.ts 선언 파일 작성

.d.ts 파일은 타입 선언(구현 없음)을 포함합니다 — JavaScript 코드의 타입을 설명합니다. 타입이 없는 npm 패키지에 타입을 추가하려면 'declare module'을 사용하세요. 'declare global'은 Window 같은 전역 타입을 확장합니다. 앰비언트 선언은 TypeScript에게 '이것은 런타임에 존재해, 믿어'라고 알립니다. 이것이 레거시 JS, 브라우저 API, 빌드 타임 주입 변수를 타입 시스템으로 통합하는 방법입니다.

typescript
// types/my-module.d.ts — describes the types of a JS library

// Declare a module (for libraries without types)
declare module "untyped-lib" {
  export function greet(name: string): string;
  export const version: string;
  export interface Config {
    timeout: number;
    retries: number;
  }
}

// Ambient declarations for global variables
declare global {
  interface Window {
    myCustomProp: string;
    myApp: { init: () => void };
  }
}

// Now window.myCustomProp is typed
// window.myCustomProp; // string

// Declaring a global function
declare function myGlobalFn(x: number): string;

// Declaring a global namespace
declare namespace MyLib {
  function doSomething(): void;
  const version: string;
}

// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constants

모듈 보강 (기존 타입 확장)

모듈 보강은 다른 모듈의 기존 타입을 확장합니다 — 원래 소스를 수정하지 않고 인터페이스에 속성을 추가합니다. 이것이 Express 미들웨어(passport 같은)가 req.user를 추가하는 방법이며, 서드파티 라이브러리 타입을 확장하는 방법입니다. 'declare module' 구문은 모듈의 타입 공간을 다시 엽니다. 보강은 전역으로 적용되려면 모듈(import/export가 있는 파일) 내에 있어야 합니다.

typescript
// Module augmentation: add to an existing module's types

// Extend an interface from another module
import express from "express";

declare module "express" {
  interface Request {
    user?: {
      id: number;
      name: string;
    };
    // Now req.user is typed on all Express requests
  }
}

// Augment a third-party module
declare module "axios" {
  export interface AxiosRequestConfig {
    retryCount?: number; // add a custom config option
  }
}

// Augment a global interface
declare global {
  interface Array<T> {
    last(): T | undefined; // add a custom array method
  }
}

// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — now typed!

// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.

트리플 슬래시 지시어

트리플 슬래시 지시어(///)는 TypeScript에게 추가 파일이나 타입 패키지를 포함하도록 지시하는 특수 컴파일러 주석입니다. 가장 일반적인 것은 @types/node를 포함하기 위한 /// <reference types='node' />입니다. 현대 tsconfig.json의 'types'와 'lib' 옵션으로, 이들은 거의 필요 없습니다 — 설정 기반 설정을 선호하세요. 주로 .d.ts 파일과 레거시 코드에서 봅니다. 이해하면 선언 파일을 읽을 때 도움이 됩니다.

typescript
// Triple-slash directives are special comments processed by TS

/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)

/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)

/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")

// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />

declare function readFile(path: string): Buffer; // Buffer from @types/node

// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.

// Prefer tsconfig.json settings:
// {
//   "compilerOptions": {
//     "types": ["node"],      // instead of /// <reference types="node" />
//     "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
//   }
// }

패키지와 함께 타입 게시

npm 패키지와 함께 TypeScript 타입을 게시하려면, package.json의 'types'를 .d.ts 파일로 설정하고 tsconfig에서 'declaration: true'를 활성화하세요. 소비자는 패키지를 설치할 때 자동으로 타입을 받습니다. declarationMap은 '정의로 이동'을 활성화하여 소스 .ts 파일로 점프합니다. 타입이 번들되지 않은 라이브러리를 위해, DefinitelyTyped 프로젝트(@types/package)가 커뮤니티 관리 선언을 제공합니다.

typescript
// package.json for a library with TypeScript types
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  // <-- points to the type declarations
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

// tsconfig.json for building a library
{
  "compilerOptions": {
    "declaration": true,        // generate .d.ts files
    "declarationMap": true,     // generate .d.ts.map (source maps for types)
    "sourceMap": true,          // generate .js.map
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true           // enable project references
  }
}

// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!

// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express

// Check if types exist: https://www.typescriptlang.org/dt/search

타입 전용 임포트 & 내보내기

'import type'은 타입 정보만 임포트합니다 — 런타임에 완전히 지워져, 번들 크기를 줄이고 순환 종속성 문제를 방지합니다. 인터페이스, 타입 별칭, 타입 전용 재내보내기에 사용하세요. 인라인 'import { x, type Y }' 구문(TS 4.5+)은 값과 타입 임포트를 깔끔하게 혼합합니다. verbatimModuleSyntax(TS 5.0+)은 이를 엄격하게 강제합니다. 타입 위치에만 사용되는 것을 임포트할 때는 항상 'import type'을 선호하세요.

typescript
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";

// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies

// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only

// 'export type' re-exports types only
export type { User, Config } from "./types";

// Interface vs Type for declarations
interface IUser { name: string; }  // can be augmented/merged
type TUser = { name: string };     // cannot be merged, more flexible

// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
//   "compilerOptions": { "verbatimModuleSyntax": true }
// }
20

tsconfig.json 옵션

핵심 컴파일러 옵션

tsconfig.json은 TypeScript가 컴파일하는 방법을 제어합니다. 'target'은 출력 JS 버전을 설정합니다; 'module'은 모듈 시스템을 설정합니다. 'strict: true'는 가장 중요한 설정입니다 — 모든 엄격 타입 검사(noImplicitAny, strictNullChecks 등)를 활성화합니다. 'lib'는 사용 가능한 내장 API를 결정합니다(브라우저용 DOM, 현대 JS 기능용 ES2022). 새 프로젝트는 항상 strict: true로 시작하세요.

typescript
{
  "compilerOptions": {
    "target": "ES2022",        // JS version to compile to
    "module": "ESNext",        // module system (ESNext, CommonJS, etc.)
    "moduleResolution": "bundler", // how modules are resolved
    "lib": ["ES2022", "DOM"],  // available type definitions
    "outDir": "./dist",        // output directory for compiled JS
    "rootDir": "./src",        // root of source files
    "sourceMap": true,         // generate .map files for debugging
    "declaration": true,       // generate .d.ts files (for libraries)
    "removeComments": true,    // strip comments from output

    // Type checking strictness
    "strict": true,            // enable ALL strict checks (recommended)
    "noImplicitAny": true,     // error on implicit 'any'
    "strictNullChecks": true,  // null/undefined are separate types
    "noUnusedLocals": true,    // error on unused local variables
    "noUnusedParameters": true,// error on unused function params
    "noImplicitReturns": true, // error if not all paths return
    "noFallthroughCasesInSwitch": true,

    "esModuleInterop": true,   // allow default imports from CommonJS
    "skipLibCheck": true,      // skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true
  }
}

엄격 모드 플래그 설명

엄격 모드는 엄격성 플래그의 묶음입니다. strictNullChecks가 가장 영향력 있습니다 — null/undefined를 별도 타입으로 만들어, 명시적으로 처리하게 합니다(런타임 충돌의 #1 원인). noImplicitAny는 조용한 타입 침식을 방지합니다. strictPropertyInitialization은 초기화되지 않은 클래스 필드를 잡습니다(확정 할당을 위해 ! 사용 또는 생성자에서 초기화). 새 프로젝트에서는 항상 엄격 모드를 활성화하세요 — 초기 비용이 안전성 가치가 있습니다.

typescript
{
  "compilerOptions": {
    // 'strict: true' enables ALL of these:

    "strictNullChecks": true,
    // null and undefined are NOT assignable to other types
    // without explicit union. Forces null handling.
    let x: string = null; // ERROR (without this, it's allowed)

    "noImplicitAny": true,
    // Parameters/variables can't be implicitly 'any'
    function fn(x) { } // ERROR: x is implicitly any

    "strictFunctionTypes": true,
    // Function parameter types checked contravariantly
    // (catches unsafe function assignments)

    "strictBindCallApply": true,
    // bind/call/apply are type-checked

    "strictPropertyInitialization": true,
    // Class properties must be initialized or declared with !
    class User {
      name: string; // ERROR: not initialized
      name2!: string; // OK: definite assignment assertion
    }

    "noImplicitThis": true,
    // 'this' must have a known type (no implicit any)

    "alwaysStrict": true
    // Emit "use strict" in every file
  }
}

모듈 해석 전략

moduleResolution은 임포트 경로가 해석되는 방법을 제어합니다. 'node'는 고전적 전략입니다; 'bundler'(TS 5.0+)는 Vite 같은 현대 번들러와 일치하며 package.json exports를 지원합니다. 'nodenext'는 엄격한 ESM입니다(확장자 필요). paths는 임포트 별칭을 만듭니다(@/ → src/), 번들러 설정(예: Vite의 resolve.alias)에서도 반영되어야 합니다. baseUrl + paths는 깊은 상대 임포트(../../../)를 피하는 표준 방법입니다.

typescript
{
  "compilerOptions": {
    // How TS resolves import paths

    "moduleResolution": "node",    // classic Node.js resolution
    // Looks for: file.ts, file/index.ts, node_modules/file

    "moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
    // Matches how bundlers resolve: supports import maps,
    // conditional exports, no file extension requirement

    "moduleResolution": "nodenext", // Node.js ESM resolution (strict)
    // Requires file extensions in imports: import "./foo.js"

    // Path mapping (aliases)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],           // import "@/components/Button"
      "@utils/*": ["src/utils/*"],
      "@components": ["src/components/index.ts"]
    }

    // rootDirs: virtual directories that map to the same location
    "rootDirs": ["src", "generated"],
    // imports between src/ and generated/ resolve as if same dir
  }
}

// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.ts

프로젝트 참조 (모노레포)

프로젝트 참조는 대형 코드베이스를 독립적으로 컴파일되는 하위 프로젝트로 분할합니다 — 모노레포에 필수적입니다. 각 프로젝트는 composite: true를 가지며 선언을 내보냅니다. 참조는 프로젝트 간 종속성을 선언합니다. tsc --build(-b)는 종속성 순서대로 컴파일하며, 변경된 것만 재구축합니다. 이는 대형 코드베이스의 타입 검사를 극적으로 가속화하고 패키지 간 아키텍처 경계를 강제합니다.

typescript
// tsconfig.json (root — references sub-projects)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,          // required for project references
    "declaration": true,        // must emit declarations
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { /* ... */ },
  "references": [
    { "path": "../shared" }     // depends on shared
  ]
}

// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically

// Build command: tsc --build (or tsc -b)

일반적인 tsconfig 레시피

다른 프로젝트 타입은 다른 tsconfig 설정이 필요합니다. React/Vite는 jsx: 'react-jsx'와 noEmit을 사용합니다(Vite가 컴파일). Node.js는 CommonJS(또는 ESM을 위해 NodeNext)와 types: ['node']를 사용합니다. 라이브러리는 .d.ts 출력을 위해 declaration: true가 필요하며, 더 넓은 호환성을 위해 더 낮은 target을 사용합니다. isolatedModules는 Vite/esbuild에 필요합니다(각 파일이 독립적으로 컴파일 가능해야 함). 빌드에서 테스트 파일과 node_modules을 항상 제외하세요.

typescript
// React + Vite project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",          // React 17+ JSX transform
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,             // Vite handles compilation
    "isolatedModules": true,    // required by Vite/esbuild
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

// Node.js backend project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",        // or NodeNext for ESM
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "types": ["node"],           // @types/node
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

// Library project (publishes to npm)
{
  "compilerOptions": {
    "target": "ES2020",          // broader compatibility
    "module": "ESNext",
    "declaration": true,         // emit .d.ts
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
21

데코레이터

클래스 데코레이터

클래스 데코레이터는 생성자를 받고 수정된 클래스를 반환할 수 있습니다. 실험적입니다(experimentalDecorators: true 필요). NestJS와 TypeORM에서 흔합니다.

typescript
function logged<T extends new (...args: any[]) => any>(con: T) {
    return class extends con {
        created = new Date().toISOString();
    };
}
@logged
class Service { constructor(public name: string) {} }

메서드 데코레이터

메서드 데코레이터는 (target, key, descriptor)를 받습니다. descriptor.value를 래핑하여 로깅, 캐싱, 검증을 가능하게 합니다. 이것이 NestJS 인터셉터가 작동하는 방식입니다.

typescript
function log(target: any, key: string, desc: PropertyDescriptor) {
    const orig = desc.value;
    desc.value = function(...args: any[]) {
        console.log(`Calling ${key}`, args);
        return orig.apply(this, args);
    };
}
class Calc { @log add(a: number, b: number) { return a + b; } }

속성 데코레이터

속성 데코레이터는 (target, key)를 받습니다. Object.defineProperty를 사용하여 검증을 위한 getter/setter를 만듭니다. DTO 검증을 위해 class-validator에서 사용됩니다.

typescript
function required(target: any, key: string) {
    let val: any;
    Object.defineProperty(target, key, {
        get() { return val; },
        set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
    });
}

매개변수 데코레이터

매개변수 데코레이터는 (target, methodKey, parameterIndex)를 받습니다. 검증을 위한 메타데이터 리플렉션과 함께 사용됩니다. class-validator와 NestJS가 이를 사용합니다.

typescript
function Min(min: number) {
    return (target: any, key: string, idx: number) => {
        console.log(`${key} param ${idx} >= ${min}`);
    };
}
class Order { create(@Min(0) qty: number) { return qty; } }

데코레이터 팩토리

데코레이터 팩토리는 설정을 가능하게 하는 데코레이터 함수를 반환합니다. 외부 함수는 매개변수를 받고, 내부 함수가 실제 데코레이터입니다.

typescript
function Retry(times: number) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
        const orig = desc.value;
        desc.value = async function(...args: any[]) {
            for (let i = 0; i < times; i++)
                try { return await orig.apply(this, args); }
                catch (e) { if (i === times - 1) throw e; }
        };
    };
}
22

모듈 보강

내장 타입 보강

모듈 보강은 기존 타입을 확장합니다. declare global은 Array 같은 내장 타입을 보강할 수 있게 합니다. 런타임 구현도 제공되어야 합니다.

typescript
declare global {
    interface Array<T> {
        last(): T | undefined;
        chunk(size: number): T[][];
    }
}
Array.prototype.last = function() { return this[this.length - 1]; };

라이브러리 타입 보강

모듈 보강은 서드파티 라이브러리의 타입을 확장합니다. declare module은 모듈 타입을 다시 엽니다. 프레임워크 객체에 커스텀 속성을 추가하는 데 필수적입니다.

typescript
declare module 'express' {
    interface Request {
        user?: { id: string; role: string };
    }
}
app.get('/profile', (req, res) => {
    const userId = req.user?.id;  // Typed!
});

Window 보강

Window를 보강하면 타입 안전하게 커스텀 전역 속성을 추가합니다. 앱 상태를 디버깅 도구나 분석에 노출하는 데 유용합니다.

typescript
declare global {
    interface Window {
        myApp: { init: () => void; version: string };
    }
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };

CSS 모듈

CSS 모듈은 타입 선언이 필요합니다. 선언은 .module.css 임포트를 클래스 이름의 레코드로 매핑합니다. CSS 클래스 참조에 대한 자동 완성을 가능하게 합니다.

typescript
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />

Vue 플러그인

Vue와 다른 프레임워크는 플러그인 타이핑을 위해 모듈 보강을 사용합니다. ComponentCustomProperties는 인스턴스 속성을 추가합니다. 타입 안전한 플러그인을 가능하게 합니다.

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

선언 병합

인터페이스 병합

같은 이름의 인터페이스는 자동으로 병합됩니다. 모든 멤버가 단일 인터페이스의 일부가 됩니다. 인터페이스를 여러 파일로 분할하는 데 유용합니다.

typescript
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };

네임스페이스 병합

같은 이름의 네임스페이스는 내보내기를 병합합니다. 이는 네임스페이스 내용을 여러 파일로 분할할 수 있게 합니다. 새 코드에는 ES 모듈이 권장됩니다.

typescript
namespace Utils {
    export function log(msg: string) { console.log(msg); }
}
namespace Utils {
    export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');

함수와 네임스페이스

네임스페이스는 함수, 클래스, 열거형과 병합될 수 있습니다. 네임스페이스는 함수에 정적 속성을 추가합니다. Moment.js와 유사한 라이브러리에서 사용됩니다.

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

클래스와 병합

네임스페이스를 클래스와 병합하면 정적 멤버와 중첩 타입이 추가됩니다. 네임스페이스는 중첩 타입이 되는 인터페이스를 내보낼 수 있습니다.

typescript
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
    export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };

허용되지 않는 병합

클래스는 다른 클래스와 병합할 수 없습니다. 변수는 병합할 수 없습니다. 함수는 오버로드로 병합됩니다. 열거형은 네임스페이스와 병합할 수 있습니다.

typescript
// Cannot merge classes
class A { x = 1; }
class A { y = 2; }  // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }
24

타입 좁히기

typeof & instanceof

typeof는 원시 타입을 좁힙니다. instanceof는 클래스 타입을 좁힙니다. TypeScript는 이 검사를 이해하고 각 브랜치에서 타입을 좁힙니다.

typescript
function process(value: string | number | Date) {
    if (typeof value === 'string') return value.toUpperCase();
    if (typeof value === 'number') return value.toFixed(2);
    if (value instanceof Date) return value.toISOString();
}

in 연산자

in 연산자는 속성이 존재하는지 검사하여 타입을 좁힙니다. 다른 속성 이름을 가진 구별된 유니언에 유용합니다.

typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
    if ('meow' in animal) animal.meow();
    else animal.bark();
}

구별된 유니언

구별된 유니언은 공통 속성(판별자)을 사용하여 타입을 좁힙니다. 철저한 검사를 위해 판별자를 switch하세요. 변형 타입에 가장 안전한 패턴입니다.

typescript
type Result =
    | { status: 'success'; data: string }
    | { status: 'error'; message: string };
function handle(r: Result) {
    switch (r.status) {
        case 'success': console.log(r.data); break;
        case 'error': console.log(r.message); break;
    }
}

타입 술어

타입 술어(x is T)는 커스텀 좁히기 함수를 가능하게 합니다. true 반환은 T로 좁히고, false 반환은 제외된 타입으로 좁힙니다. TypeScript는 술어를 맹목적으로 신뢰합니다.

typescript
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
    if (isFish(pet)) pet.swim();
    else pet.fly();
}

어설션 함수

어설션 함수는 조건이 실패하면 예외를 던져, 이후 코드의 타입을 좁힙니다. asserts x is T는 T로 좁힙니다. 중복 null 검사를 제거합니다.

typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
    if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
    assertDefined(user);
    console.log(user.name);  // User (not undefined)
}
25

템플릿 리터럴 타입

기본 템플릿 리터럴

템플릿 리터럴 타입은 문자열 패턴을 만듭니다. 문자열이 템플릿과 일치하도록 제한합니다. API 엔드포인트와 이벤트 이름에 대한 타입 안전한 패턴을 가능하게 합니다.

typescript
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world';  // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';

Uppercase & Lowercase

내장 내장 타입이 문자열 리터럴 타입을 변환합니다. 템플릿 리터럴과 결합하여 타입 안전한 이벤트 이름과 상수를 생성하세요.

typescript
type Upper = Uppercase<'hello'>;  // 'HELLO'
type Lower = Lowercase<'WORLD'>;  // 'world'
type Cap = Capitalize<'foo'>;     // 'Foo'
type EventName = `on${Capitalize<'click'>}`;  // 'onClick'

키 리매핑

키 리매핑(as 절)은 매핑된 타입 중에 키를 변환합니다. 속성 이름에서 getter/setter 이름을 생성합니다. 인터페이스에서 타입 안전한 API를 만듭니다.

typescript
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

문자열 패턴 매칭

infer가 있는 템플릿 리터럴 타입은 컴파일 타임에 문자열을 파싱할 수 있습니다. Split은 문자열을 튜플로 분해합니다. 타입 안전한 문자열 조작을 가능하게 합니다.

typescript
type Split<S extends string, D extends string> =
    S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>;  // ['a', 'b', 'c']

이벤트 시스템 타이핑

제네릭이 있는 템플릿 리터럴 타입은 완전히 타입 안전한 이벤트 시스템을 만듭니다. 이벤트 이름이 페이로드 타입을 결정합니다. on과 emit이 일치하는 타입을 강제합니다.

typescript
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
    on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x));  // Typed!
26

infer 키워드

반환 타입 추출

infer는 조건부 타입 내에 타입 변수를 선언합니다. 특정 위치의 타입을 캡처합니다. ReturnType이 내장 동등물입니다.

typescript
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>;  // { name: string; age: number; }

Promise 타입 추출

infer는 Promise의 내부 타입을 추출합니다. DeepUnwrap은 중첩된 Promise를 재귀적으로 풉니다. 내장 Awaited<T>가 현대 TypeScript에서 이를 수행합니다.

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>;  // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>;  // boolean

배열 요소 추출

infer E는 배열의 요소 타입을 캡처합니다. 튜플의 경우, infer는 특정 위치를 캡처할 수 있습니다. 제네릭 컬렉션 작업에 유용합니다.

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>;  // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>;  // string

함수 매개변수 추출

infer P는 함수의 매개변수 튜플을 캡처합니다. Parameters가 내장 동등물입니다. 타입을 보존하면서 함수를 래핑하는 데 유용합니다.

typescript
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>;  // [string, number]

다중 infer

다중 infer 변수는 타입의 다른 부분을 동시에 캡처할 수 있습니다. 단일 조건부에서 복잡한 타입 변환을 가능하게 합니다.

typescript
type FirstLast<T extends any[]> =
    T extends [infer First, ...any[], infer Last]
        ? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>;  // { first: 1; last: 4 }
27

분산

공변성

공변성은 Dog[]를 Animal[]에 할당할 수 있게 합니다. TypeScript 배열은 공변이지만 이는 불건전합니다: Animal을 Dog[]에 push하면 배열이 손상됩니다.

typescript
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs;  // OK (covariant)
// animals.push(new Animal());  // Unsafe at runtime

반공변성

반공변성은 Dog를 받는 함수가 Animal을 받는 함수가 예상되는 곳에 사용될 수 있음을 의미합니다. Dog 핸들러가 Dog인 모든 Animal을 처리하므로 안전합니다.

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

이분산성

메서드 구문은 이분산입니다. 함수 속성 구문은 strictFunctionTypes와 함께 반공변입니다. 메서드는 OO 호환성을 위해 이분산입니다.

typescript
interface IFace {
    method(x: Animal): void;  // bivariant
    fn: (x: Animal) => void;  // contravariant
}
class Impl implements IFace {
    method(x: Dog) {}  // OK (bivariant)
    fn = (x: Dog) => {}  // Error (contravariant)
}

in/out 분산

TypeScript 4.7+은 명시적 분산 어노테이션을 지원합니다. in은 반공변(소비자), out은 공변(생산자), in out은 불변(둘 다)을 표시합니다.

typescript
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }

불변 타입

불변 타입은 정확한 타입 일치가 필요합니다. 타입이 입력과 출력 위치 모두에 나타날 때 불변입니다. Box<Dog>는 Box<Animal>에 할당할 수 없습니다.

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

빌더 패턴

플루언트 빌더

빌더 패턴은 복잡한 객체를 단계별로 구축합니다. 각 메서드는 체이닝을 위해 this를 반환합니다. SQL 쿼리, HTTP 요청, 설정에 유용합니다.

typescript
class QueryBuilder {
    private parts: string[] = [];
    select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
    from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
    where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
    build(): string { return this.parts.join(' '); }
}

타입 안전 빌더

타입 안전 빌더는 조건부 타입을 사용하여 필수 필드를 강제합니다. build()는 hasName이 true일 때만 Person을 반환합니다. 컴파일 타임에 누락된 필드를 잡습니다.

typescript
interface State { hasName: boolean; }
class Builder<S extends State> {
    name(n: string): Builder<S & { hasName: true }> { return this as any; }
    build(): S extends { hasName: true } ? Person : never { return {} as any; }
}

불변 빌더

불변 빌더는 각 수정마다 새 인스턴스를 만듭니다. 타입 시스템은 인터섹션 타입을 통해 추가된 모든 키를 추적합니다. 각 set은 새 빌더 타입을 반환합니다.

typescript
class ImmutableBuilder<T extends object> {
    constructor(private data: T) {}
    set<K extends string, V>(key: K, value: V):
        ImmutableBuilder<T & { [P in K]: V }> {
        return new ImmutableBuilder({ ...this.data, [key]: value });
    }
    build(): T { return this.data; }
}

디렉터 패턴

디렉터는 일반적인 구축 시퀀스를 캡슐화합니다. 빌더를 사용하여 표준 제품을 만듭니다. 다른 디렉터가 다른 변형을 생산합니다.

typescript
class HTMLBuilder {
    private html = '';
    addTag(tag: string, content: string): this {
        this.html += `<${tag}>${content}</${tag}>`; return this;
    }
    build(): string { return this.html; }
}
class Director {
    buildPage(title: string, body: string): string {
        return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
    }
}

스텝 빌더

스텝 빌더는 타입 시스템을 통해 특정 메서드 호출 순서를 강제합니다. 각 단계는 다음 메서드만 사용 가능한 다른 타입을 반환합니다.

typescript
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
    constructor(private name: string) {}
    age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
    constructor(private name: string, private age: number) {}
    build() { return { name: this.name, age: this.age }; }
}
29

TypeScript로 테스팅

TypeScript와 Jest

TypeScript 테스트를 위해 ts-jest 또는 @swc/jest를 사용하세요. describe는 관련 테스트를 그룹화하고, it는 테스트 케이스를 정의합니다. expect는 toBe, toEqual 같은 매처로 어설션을 만듭니다.

typescript
import { sum } from './sum';
describe('sum', () => {
    it('adds two numbers', () => {
        expect(sum(1, 2)).toBe(3);
    });
    it('handles negatives', () => {
        expect(sum(-1, -2)).toBe(-3);
    });
});

타입 테스팅

expectTypeOf는 컴파일 타임에 타입을 테스트합니다. 반환 타입, 매개변수 타입, 해결된 promise 타입을 검증합니다. 타입이 잘못되면 빌드가 실패합니다.

typescript
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
    const fn = {} as unknown as GetUser;
    expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
    expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});

타입으로 모킹

jest.Mocked<T>는 인터페이스에서 타입화된 모의 객체를 만듭니다. jest.fn()은 타입화된 반환 값으로 모의 함수를 만듭니다. 모의 객체는 완전히 타입화됩니다.

typescript
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
    find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });

테스트 설정 & 해체

beforeAll은 모든 테스트 전에 한 번 실행되고, afterAll은 후에 한 번 실행됩니다. beforeEach는 각 테스트 전에, afterEach는 각 테스트 후에 실행됩니다. 설정과 정리에 사용하세요.

typescript
describe('Database', () => {
    beforeAll(async () => { db = createDatabase(); await db.connect(); });
    afterAll(async () => { await db.disconnect(); });
    beforeEach(async () => { await db.clear(); });
    afterEach(() => { jest.restoreAllMocks(); });
});

속성 기반 테스팅

속성 기반 테스팅은 무작위 입력을 생성하여 불변성을 테스트합니다. fc.assert는 속성을 여러 번 실행합니다. 예제 기반 테스트가 놓치는 에지 케이스를 잡습니다.

typescript
import { fc } from 'fast-check';
describe('sort', () => {
    it('preserves length', () => {
        fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
            return [...arr].sort().length === arr.length;
        }));
    });
});
30

일반적인 함정

any vs unknown

any는 타입 검사를 비활성화하여 버그를 숨깁니다. unknown은 타입 안전합니다: 사용하기 전에 반드시 좁혀야 합니다. 신뢰할 수 없는 소스(API, JSON.parse)에는 unknown을 사용하세요.

typescript
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data)
        return (data as any).foo;
}

초과 속성 검사

TypeScript는 직접 할당된 객체 리터럴의 초과 속성만 검사합니다. 변수를 통하면 검사가 건너뜁니다. 더 엄격한 런타임 검증을 위해 zod를 사용하세요.

typescript
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true };  // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data;  // OK

열거형 vs 유니언

열거형은 역방향 매핑이 있는 런타임 객체를 만듭니다. 유니언 타입은 런타임 비용이 없고 트리 쉐이킹 가능합니다. 새 코드에는 유니언 타입을 선호하세요.

typescript
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }

구조적 타이핑

TypeScript는 구조적 타이핑을 사용합니다: 형태가 일치하면 타입이 호환됩니다. Admin은 User에 할당 가능합니다. 이는 논리적 버그를 일으킬 수 있습니다. 브랜드 타입이 명목적 구분을 추가합니다.

typescript
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin;  // OK (structural)

타입 어설션의 위험

타입 어설션(as)은 런타임 검사 없이 TypeScript를 재정의합니다. 외부 데이터에는 런타임 검증(zod, io-ts)을 사용하세요. safeParse는 예외를 던지지 않고 결과를 반환합니다.

typescript
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.