Menu

TypeScript 제네릭: 제네릭 함수, 타입, 클래스

제네릭은 함수, 인터페이스, 타입, 클래스가 여러 타입과 함께 동작하면서도 그 관계를 유지하게 합니다. 들어가는 것이 나오는 것을 정합니다. 제네릭 함수, 타입 인수 추론, 여러 타입 매개변수, 제네릭 인터페이스와 클래스, 기본값, 그리고 제네릭을 쓰지 말아야 할 때를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

제네릭은 타입 매개변수입니다. 제네릭 함수는 보통 T라고 부르는 자리 표시자 타입을 선언하고, 호출할 때마다 그것이 채워지므로, 들어가는 타입이 나오는 타입을 정합니다. 그러면 함수 하나가 숫자, 문자열, 직접 만든 객체에 모두 동작하면서도 모든 호출이 완전히 타입을 갖습니다.

함수 이름 뒤의 <T>가 타입 매개변수를 선언합니다. items: T[]는 그것을 입력에, T | undefined는 출력에 써서 둘을 연결합니다. number나 string을 쓴 적이 없습니다. TypeScript가 인수에서 T를 추론했습니다.

왜 any나 unknown이 아닌가?

제네릭이 없다면 매개변수를 any나 unknown으로 지정했을 것입니다. 둘 다 모든 배열을 받고, 둘 다 입력과 출력 사이의 연결을 잃습니다.

매개변수 타입모든 입력을 받음결과 타입검사됨
any예any아니요: 실수가 컴파일됨
unknown예unknown예, 하지만 쓰기 전에 좁혀야 함
T(제네릭)예입력에 묶임예, 정확한 타입으로

타입 인수 추론

대개 TypeScript는 넘긴 값에서 타입 인수를 추론합니다. 호출할 때 꺾쇠괄호로 직접 쓸 수도 있는데, 인수 어디에도 T가 나오지 않거나 추론이 원하지 않는 것을 고를 때 필요합니다.

알아 둘 만한 추론 결과가 두 가지 있습니다. 명시적 인수 없는 pair(1, "one")은 오류입니다. TypeScript가 첫 인수에서 T = number를 추론한 뒤 Argument of type 'string' is not assignable to parameter of type 'number'.(TS2345)를 보고합니다. 그리고 인수가 전혀 없는 emptyList()는 T = unknown이 되어 unknown[]를 주는데, 원하는 경우는 드뭅니다.

여러 타입 매개변수

함수는 필요한 만큼 타입 매개변수를 쉼표로 구분해 선언할 수 있습니다. 각각은 독립적으로 추론됩니다.

mapValues는 세 가지를 한꺼번에 추론합니다. K는 "tea" | "cake", V는 number, R은 콜백의 반환값에서 string입니다. K extends string은 제약으로, 다음 페이지에서 다룹니다.

제네릭 인터페이스와 타입 별칭

타입도 매개변수를 받을 수 있습니다. 그러면 타입을 쓸 때 인수를 씁니다: Box<number>, ApiResponse<User>. 대부분의 내장 컬렉션 타입이 이렇게 동작합니다: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.

Result<T, E = string>은 기본 타입 매개변수 도 보여 줍니다. Result<number>는 Result<number, string>을 뜻합니다. 선택적 함수 매개변수처럼, 기본값이 있는 매개변수는 필수 매개변수 뒤에 와야 합니다.

제네릭 클래스

클래스는 이름 뒤에 타입 매개변수를 받고, 인스턴스마다 그것이 정해집니다. 필드, 메서드, 생성자 매개변수 모두 그것을 쓸 수 있습니다.

생성자가 T를 받는다면 명시적 인수를 생략할 수 있습니다. new Box(5)는 Box<number>로 추론됩니다. 정적 멤버는 인스턴스가 아니라 클래스 자체에 속하므로 클래스의 타입 매개변수를 쓸 수 없습니다(Static members cannot reference class type parameters., TS2302).

제네릭 화살표 함수와 함수 타입

타입 매개변수 목록은 매개변수 목록 앞에 옵니다. 같은 문법으로 제네릭 함수 타입을 기술합니다.

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);

// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];

const 타입 매개변수로 리터럴 타입 추론하기

기본적으로 제네릭은 넓어진 타입을 추론합니다. pair("a", "b")는 [string, string]을 줍니다. 정확한 값이 중요할 때(라우트 이름, 이벤트 이름, 열 목록) 매개변수를 const로 표시하면(TypeScript 5.0 이상) 인수를 as const로 쓴 것처럼 추론합니다.

호출하는 쪽은 평범한 배열을 쓰고, 함수는 리터럴 타입을 유지합니다.

제네릭을 쓰지 말아야 할 때

타입 매개변수는 두 가지를 연결할 때 제 몫을 합니다. 매개변수와 반환 타입, 두 매개변수, 또는 매개변수와 콜백입니다. T가 한 번만 나온다면 아무것도 더하지 않으며, 평범한 타입이 더 명확합니다.

// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
  console.log(value);
}

// Clearer
function logValueSimple(value: unknown): void {
  console.log(value);
}

// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

마지막 것은 흔하면서 오해를 부릅니다. parseJson<User>(text)는 타입 안전해 보이지만 JSON.parse(text) as User와 똑같이 아무것도 검사하지 않습니다. 대신 unknown을 반환하고 검증하세요. 남용의 다른 신호: 항상 같은 인수를 받는 타입 매개변수, 또는 알려진 두 타입의 유니언으로 대체할 수 있는 타입 매개변수입니다.

T에 대해 무언가를 알아야 하는 제네릭(length가 있다거나, K가 T의 키라거나)이라면 extends를 쓰는 제약이 다음 단계입니다.

자주 묻는 질문

TypeScript에서 제네릭이란 무엇인가요?

제네릭은 타입 매개변수입니다. T 같은 자리 표시자로, 함수, 인터페이스, 타입 별칭, 클래스를 쓸 때마다 실제 타입으로 채워집니다. function first<T>(items: T[]): T | undefined는 어떤 배열에든 동작하고, 결과는 넘긴 배열의 요소 타입을 가지므로 first([1, 2])는 number | undefined, first(["a"])는 string | undefined입니다.

제네릭과 any의 차이는 무엇인가요?

any는 타입 검사를 끕니다. 들어가는 값과 나오는 값 사이에 아무 관계가 없고, 결과도 any입니다. 제네릭은 관계를 유지합니다. identity<T>(x: T): T는 넘긴 타입을 정확히 반환합니다. 무엇이든 받지만 타입을 돌려줄 필요가 없다면 unknown을, 돌려줘야 한다면 제네릭을 쓰세요.

TypeScript에서 <T>는 무슨 뜻인가요?

T라는 이름의 타입 매개변수를 선언합니다. function wrap<T>(value: T)에서 T는 TypeScript가 호출마다 인수로부터 채우거나, wrap<string>("a")처럼 직접 넘기는 타입 변수입니다. T라는 이름은 관례일 뿐이며, TItem이나 Key 같은 긴 이름도 똑같이 동작합니다.

TypeScript에서 제네릭 화살표 함수는 어떻게 쓰나요?

타입 매개변수 목록을 매개변수 앞에 둡니다: const first = <T>(items: T[]): T | undefined => items[0];. .tsx 파일에서는 <T>가 JSX 태그로 읽히므로 끝에 쉼표를 붙인 <T,>나 <T extends unknown>을 쓰세요.

TypeScript에서 제네릭의 기본 타입은 어떻게 설정하나요?

매개변수 뒤에 = Type을 붙입니다: interface ApiResponse<T = unknown> { data: T }. 그러면 인수 없는 ApiResponse는 ApiResponse<unknown>을 뜻합니다. 선택적 함수 매개변수처럼, 기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기