TS 3.3 · 3부. 인터페이스와 제네릭

제네릭

타입을 나중에 정하는 함수와 형태를 만듭니다.

예상 학습 시간 17분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

타입을 나중에 정합니다

배열의 첫 값을 꺼내는 함수를 만든다고 해 봅시다. 숫자 배열에도, 글자 배열에도 쓰고 싶습니다. 타입을 무엇으로 적을까요.

main.ts
function first(list: unknown[]): unknown {
	return list[0];
}

const b = first(["홍길동", "김철수"]);

console.log(b.toUpperCase());
출력
7:13 TS18046 'b'은(는) 'unknown' 형식입니다.

unknown 으로 적으면 어떤 배열이든 받지만, 꺼낸 값이 무엇인지 잃어버립니다. 부르는 쪽에서 다시 확인해야 합니다.

제네릭은 "넣은 것과 같은 것이 나온다" 를 적는 방법입니다.

function first<T>(list: T[]): T | undefined { … }
             // ↑ 지금은 모르지만, 부를 때 정해지는 타입
최소 예제 · 편집 가능

타입이 따라옵니다

main.ts
function first<T>(list: T[]): T | undefined {
	return list[0];
}

const a = first([1, 2, 3]);
const b = first(["홍길동", "김철수"]);

console.log(a, b);
console.log(b?.toUpperCase());
출력
1 홍길동 홍길동

<number> 처럼 적어 주지 않았는데도 a 는 숫자, b 는 글자입니다. 넘긴 배열을 보고 T 가 무엇인지 정해집니다.

b?. 를 붙인 것은 반환 타입이 string | undefined 이기 때문입니다 — 빈 배열이면 꺼낼 것이 없습니다(1.6).

상세 사용법

여러 개를 둘 수 있습니다

main.ts
function pair<K, V>(key: K, value: V): [K, V] {
	return [key, value];
}

const a = pair("age", 27);
const b = pair(1, true);

console.log(a[0], a[1]);
console.log(b[0], b[1]);
출력
age 27 1 true

이름은 아무렇게나 지어도 됩니다. T · K · V 는 그저 관례입니다 (type · key · value).

이미 사용해 왔습니다. Array<number>(1.3) 와 Promise<string> 이 제네릭입니다 — 무엇이 담기는지를 꺾쇠 안에 적는 것입니다.

상세 사용법

아무것이나 받지 않게 하기

T 로 두면 정말 무엇이든 들어옵니다. 함수 안에서 length 를 사용한다면 길이가 있는 것만 받아야 합니다.

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

console.log(longest("홍길동", "김철수입니다"));
console.log(longest([1, 2], [1, 2, 3]).length);
출력
김철수입니다 3

extends 가 여기서는 물려받는다는 뜻이 아니라 "적어도 이것은 갖춘 것" 이라는 조건입니다. 글자와 배열은 둘 다 length 를 가지고 있어 지나갑니다.

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

console.log(longest("홍길동", "김철수입니다"));
console.log(longest([1, 2], [1, 2, 3]).length);

console.log(longest(1, 2));
출력
8:21 TS2345 'number' 형식의 인수는 '{ length: number; }' 형식의 매개 변수에 할당될 수 없습니다.

숫자에는 length 가 없어 막혔습니다. 조건을 적어 두지 않았다면 함수 안쪽의 a.length 가 먼저 막혔을 것입니다.

흔한 실수

처음에 자주 걸리는 것

꺾쇠를 값 자리에 적는 것

first<string>(list) 처럼 직접 적을 수도 있지만 대개 필요 없습니다. 넘긴 값을 보고 정해집니다. 정해지지 않을 때만 적습니다.

T 를 아무 데나 두는 것

T들어오는 자리와 나가는 자리를 잇는 끈입니다. 매개 변수에 한 번도 나오지 않으면 정해질 방법이 없어 의미가 없습니다.

any 로 도망가는 것

제네릭이 어려워 any 로 적으면 그 자리의 검사가 꺼집니다(1.5). 제네릭은 검사를 유지한 채 여러 타입에 쓰는 방법입니다.

실습 문제

직접 해보기

1. 마지막 값 꺼내기 난이도 중

배열의 마지막 값을 꺼내는 제네릭 함수를 만드세요.

function last<T>(list: T[]): T | undefined {
	return list[list.length - 1];
}

console.log(last([1, 2, 3]));
console.log(last(["홍길동", "김철수"]));
console.log(last([]));
2. 조건 붙이기 난이도 상

name 을 가진 것만 받아 그 이름을 돌려주는 제네릭 함수를 만드세요.

function nameOf<T extends { name: string }>(item: T): string {
	return item.name;
}

console.log(nameOf({ name: "홍길동", age: 27 }));
console.log(nameOf({ name: "연필", price: 1000 }));
요약

이 단원의 정리

  • 제네릭은 "넣은 것과 같은 것이 나온다" 를 적는 방법입니다.
  • 꺾쇠 안의 이름은 부를 때 정해집니다.
  • 대개 직접 적지 않아도 넘긴 값을 보고 정해집니다.
  • extends 로 "적어도 이것은 갖춘 것" 이라는 조건을 답니다.
  • Array<T> · Promise<T> 도 제네릭입니다.