[TypeScript] Generics
정의
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 로 감싸기.
관련 위키
- TypeScript - 상위 개요
- Interfaces - Generic interface
- Type Aliases - Generic type alias
- Conditional Types - infer
- Mapped Types - Generic + keyof
- Utility Types - Generic 활용
- Type Narrowing
- Strict Mode - Generic 안전성
이 글의 용어 (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, …
이 개념을 다룬 위키 페이지 (13)
- wiki[PLT] Type Systems (타입 시스템)
- wiki[TypeScript] Conditional Types
- wiki[TypeScript] Declaration Files (.d.ts)
- wiki[TypeScript] Decorators
- wiki[TypeScript] Interfaces
- wiki[TypeScript] Mapped Types
- wiki[TypeScript] Type Narrowing (Type Guards)
- wiki[TypeScript] Strict Mode & tsconfig
- wiki[TypeScript] Template Literal Types
- wiki[TypeScript] Type Aliases
- wiki[TypeScript] Union & Intersection Types
- wiki[TypeScript] Utility Types
- wikiTypeScript
💬 댓글