본문으로 건너뛰기
김신건의 로그

[TypeScript] Generics

· 수정 · 📖 약 2분 · 602자/단어 #typescript #types #generics
TypeScript Generics, TS Generics, TypeScript generic function, TypeScript generic type, type parameter, TypeScript 제네릭, extends constraint, generic default

정의

Generics 는 타입을 파라미터화하는 문법입니다. 함수, 클래스, 인터페이스, type alias 가 재사용 가능 하면서도 타입 안전 하도록 만듭니다. Java 의 generic, C++ 의 template 유사.

왜 필요한가

Generic 없이:

function identity(x: any): any {
  return x;
}

const s = identity("hello");   // s: any (타입 손실)

Generic 사용:

function identity<T>(x: T): T {
  return x;
}

const s = identity("hello");   // s: "hello" (또는 string)
const n = identity(42);        // n: 42 (또는 number)

컴파일러가 T 를 호출 인자에서 추론.

개념 흐름

다이어그램 1: 제네릭 인스턴스화

flowchart TD
    GF["Generic Function of T"]
    GF -->|"string 인자로 호출"| S["T = string"]
    GF -->|"number 인자로 호출"| N["T = number"]
    GF -->|"boolean 인자로 호출"| B["T = boolean"]
    S --> R1["반환 타입: string"]
    N --> R2["반환 타입: number"]
    B --> R3["반환 타입: boolean"]

다이어그램 2: 제약, 기본값, variance

flowchart TB
    T["Type Parameter T"]
    T --> C["제약 extends"]
    T --> D["기본값 Default"]
    T --> V["분산 Variance"]
    C -->|"T extends U"| C1["U 서브타입만 허용"]
    C --> C2["예: T extends object"]
    C --> C3["예: T extends keyof U"]
    D -->|"T = DefaultType"| D1["T 생략시 기본값 사용"]
    V --> V1["공변: 출력 위치"]
    V --> V2["반변: 입력 위치"]

Generic 함수

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

const s = first(["a", "b"]);      // string | undefined
const n = first([1, 2]);           // number | undefined

명시적 타입 지정도 가능:

const x = first<number>([1, 2, 3]);

대개 추론이 정확하므로 생략.

여러 type parameter

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("hello", 42);   // [string, number]

Generic arrow function

const identity = <T,>(x: T): T => x;

.tsx 파일에서는 <T> 가 JSX 와 헷갈리므로 <T,><T extends unknown> 로 명확히.

Constraint (extends)

Type parameter 에 제약을 걸어 특정 속성/타입만 허용:

function longest<T extends {length: number}>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");            // OK
longest([1, 2, 3], [4]);            // OK
longest(1, 2);                       // 오류: number 는 length 없음

keyof constraint

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

const user = {name: "kim", age: 30};
const name = getProperty(user, "name");     // string
const age = getProperty(user, "age");       // number
getProperty(user, "email");                  // 오류

keyof T 는 T 의 속성 이름 union.

여러 constraint

function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return {...a, ...b};
}

Default type

interface Container<T = string> {
  value: T;
}

const c1: Container = {value: "hello"};      // T = string
const c2: Container<number> = {value: 42};    // T = number

React 의 useState<T = undefined> 같은 패턴에서 자주 사용.

Generic Interface

interface Repository<T> {
  find(id: number): Promise<T | null>;
  save(item: T): Promise<void>;
  delete(id: number): Promise<void>;
}

class UserRepo implements Repository<User> {
  async find(id: number): Promise<User | null> { ... }
  async save(user: User): Promise<void> { ... }
  async delete(id: number): Promise<void> { ... }
}

Generic Type Alias

type Result<T, E = Error> =
  | {ok: true; value: T}
  | {ok: false; error: E};

function parse<T>(input: string): Result<T> {
  try {
    return {ok: true, value: JSON.parse(input)};
  } catch (e) {
    return {ok: false, error: e as Error};
  }
}

Rust 의 Result 유사.

Generic Class

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];
  }
}

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

Generic Constraint 심화

Constraint + default

function fetchAll<T extends Fetchable = User>(): Promise<T[]> {
  ...
}

Constraint with union

function serialize<T extends string | number>(x: T): string {
  return String(x);
}

Generic + literal type

function log<T extends string>(level: T, msg: string): void {
  console.log(`[${level}] ${msg}`);
}

const level = log("INFO", "message");   // T = "INFO"

Generic Parameter 로 함수 오버로드 대체

// 오버로드
function process(x: string): string;
function process(x: number): number;
function process(x: any): any {
  return x;
}

// Generic
function process<T extends string | number>(x: T): T {
  return x;
}

infer 키워드

Conditional type 안에서 타입을 추출:

type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;

type A = ReturnType<() => string>;              // string
type B = ReturnType<(x: number) => number[]>;    // number[]

type FirstArg<F> = F extends (arg: infer A, ...args: any[]) => any ? A : never;
type X = FirstArg<(name: string, age: number) => void>;   // string

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Y = Awaited<Promise<Promise<string>>>;     // string

자세한 것은 Conditional Types 참조.

const type parameter (TS 5.0+)

Generic 이 리터럴 타입을 유지하도록:

function fruits<const T extends string[]>(items: T): T {
  return items;
}

const f = fruits(["apple", "banana"]);
//    ^^ readonly ["apple", "banana"]  (literal 유지)

이 없으면:

const f = fruits(["apple", "banana"]);
//    ^^ string[]  (넓혀짐)

NoInfer<T> (TS 5.4+)

Type inference 를 특정 파라미터에서 막음:

function createConfig<T>(defaults: T, overrides: NoInfer<Partial<T>>): T {
  return {...defaults, ...overrides};
}

createConfig({name: "kim", age: 30}, {name: "lee"});
// T 는 defaults 에서만 추론, overrides 는 T 에 종속

Generic 성능 팁

  • 재귀 제한: 재귀 generic 은 컴파일 시간 증가. 필요시 depth counter.
  • Union 분배: T extends U ? X : Y 에서 T 가 union 이면 자동 분배. 원치 않으면 [T] extends [U] 감쌈.
  • Alias 로 캐시: 자주 쓰는 조합은 alias 로 이름 붙이면 컴파일러가 캐시.

실전 예시

React useState

function useState<T>(initial: T): [T, (v: T) => void] {
  // ...
}

const [count, setCount] = useState(0);    // T = number
const [name, setName] = useState("");     // T = string

배열 map/filter

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

const doubled = map([1, 2, 3], x => x * 2);      // number[]
const strs = map([1, 2, 3], x => x.toString());   // string[]

Async wrapper

async function retry<T>(
  fn: () => Promise<T>,
  attempts: number = 3,
  delay: number = 1000,
): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === attempts - 1) throw e;
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}

const result = await retry(() => fetchUser(id));

Event emitter

type EventMap = {
  click: {x: number; y: number};
  keydown: {key: string};
  submit: {data: object};
};

class Emitter<E extends Record<string, any>> {
  private handlers: {[K in keyof E]?: ((v: E[K]) => void)[]} = {};

  on<K extends keyof E>(event: K, handler: (v: E[K]) => void): void {
    (this.handlers[event] ||= []).push(handler);
  }

  emit<K extends keyof E>(event: K, value: E[K]): void {
    this.handlers[event]?.forEach(h => h(value));
  }
}

const e = new Emitter<EventMap>();
e.on("click", (v) => console.log(v.x, v.y));   // v 는 {x, y} 로 좁혀짐
e.emit("click", {x: 10, y: 20});

함정

WARNING

<T> 를 .tsx 에서. <T>() => x 는 JSX 로 파싱됨. <T,> (콤마) 또는 <T extends unknown>.

CAUTION

Constraint 없이 T 를 사용. function f<T>(x: T) { x.foo(); } 는 T 에 foo 가 있다는 보장 없어 오류. T extends {foo(): void} 로 constraint.

WARNING

Generic 은 런타임에 없음. if (x instanceof T) 같은 것 불가. T 는 컴파일 시만.

IMPORTANT

Type inference 우선. 명시적 <Type> 지정을 남발하지 마세요. 추론이 정확할 때가 대부분.

CAUTION

Union 분배 함정. T extends string ? "yes" : "no" 에서 T = string | number 면 "yes" | "no" 됨. 원치 않으면 tuple 로 감싸기.

관련 위키

이 글의 용어 (8개)
[TypeScript] Conditional Typestypescript
정의 Conditional Type 은 형태로 타입 레벨에서 조건 분기를 표현합니다. 키워드로 매칭된 타입을 추출하고, union 에 대해서는 자동 분배 (distributive…
[TypeScript] Interfacestypescript
정의 Interface 는 객체의 shape (형태) 을 정의하는 TypeScript 문법입니다. 필드, 메서드, 인덱스 시그니처, 호출 시그니처를 선언하며, 로 상속하고 dec…
[TypeScript] Mapped Typestypescript
정의 Mapped Type 은 기존 타입의 각 필드를 순회 ( ) 하며 새 타입을 생성하는 문법입니다. , modifier 를 붙이거나 제거 ( , ) 하고, clause 로 키…
[TypeScript] Strict Mode & tsconfigtypescript
정의 Strict Mode 는 여러 엄격한 타입 검사 플래그를 한 번에 켜는 옵션 ( ) 입니다. 강력한 타입 안전성을 제공하지만, 기존 코드베이스에는 대량의 오류를 발생시킬 수…
[TypeScript] Type Aliasestypescript
정의 Type Alias 는 타입에 이름을 붙이는 TypeScript 문법 ( ). Interface 는 객체 shape 만 표현하지만, type alias 는 모든 타입 표현식…
[TypeScript] Type Narrowing (Type Guards)typescript
정의 Type Narrowing 은 TypeScript 컴파일러가 제어 흐름 분석 (control flow analysis) 을 통해 특정 위치에서 값의 타입을 더 좁게 추론하는…
[TypeScript] Utility Typestypescript
정의 Utility Types 는 TypeScript 표준 라이브러리에 포함된 미리 정의된 generic 타입 들입니다. 기존 타입을 변형/구성하는 흔한 패턴을 제공하며, map…
TypeScripttypescript
정의 TypeScript 는 Microsoft 가 2012년 발표한 JavaScript 의 상위 집합 프로그래밍 언어입니다. 정적 타입 시스템, 인터페이스, 제네릭, enum, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기