제네릭(generics)은 함수에 비워 둔 타입 칸 T를 두고, 부를 때마다 넘긴 인자에 맞춰 그 칸을 채우는 문법입니다. 함수 하나를 글자 배열에도 수 배열에도 쓰면서, 돌려받는 값의 타입은 그대로 지킵니다.
함수의 입력과 출력에서는 매개변수마다 타입을 하나씩 적었습니다. 그러면 그 타입의 값만 넘길 수 있어서, 하는 일이 같아도 값의 타입마다 함수를 따로 써야 합니다.
이 단원은 타입을 비워 두었다가 부를 때 채우는 칸을 다룹니다. 이 레슨에서는 T가 언제 무엇으로 채워지는지 보고, 검사 규칙 7을 세웁니다.
JS 다시 보기: setTimeout에 넘긴 콜백은 지금 코드가 다 끝난 뒤에 불려 그때의 바깥 변수 값을 읽고, await는 Promise가 끝나기를 기다려 값을 꺼냅니다. 이 단원 끝 레슨 Promise와 async의 타입이 이 셋을 씁니다(JS '나중에 하기: setTimeout' · JS '클로저: 만들어진 칸을 기억하는 함수' · JS 'async와 await').
이 단원을 이미 안다면 단원 연습 문제의 진단 문항부터 풀고 틀린 레슨만 보세요.
1타입을 하나 적으면 그 타입만 받는다
배열의 첫 원소를 꺼내는 함수를 메뉴 이름에 쓰려고 매개변수를 string[]로 적었습니다. 같은 함수로 가격 배열의 첫 원소도 꺼내 봅니다.
1~3번 줄은 폰 화면에 맞추려고 매개변수에서 줄을 나눈 함수 머리이고, 한 줄로 쓴 머리와 같습니다.
function first(
items: string[],
) {
return items[0];
}
const a = first(['커피', '라테']);
const b = first([4000, 4500]);
console.log(a, b);
main.ts(7,18): error TS2322: Type 'number' is not assignable to type 'string'.main.ts(7,24): error TS2322: Type 'number' is not assignable to type 'string'.커피 40007번 줄이 두 번 막힙니다. 열 18과 24는 4000과 4500이 선 자리이고, 오류 문장은 'number 타입은 string 타입에 넣을 수 없다'는 뜻입니다. 매개변수가 string[]라 원소 하나하나가 string 자리에 들어가야 하는데 수가 왔습니다.
검사 오류가 있어도 Node는 타입을 지우고 돌리므로 커피 4000이 찍힙니다(검사 규칙 1). 함수가 하는 일은 수 배열에도 맞는데, 적어 둔 타입이 막았습니다.
수 배열용 함수를 하나 더 쓰면 같은 몸을 두 번 적게 됩니다. 매개변수를 any[]로 적으면 둘 다 받지만, 돌려받는 값도 any가 되어 검사가 꺼집니다(자주 틀리는 곳).
필요한 것은 타입을 비워 두었다가, 부를 때 넘긴 배열에 맞춰 채우는 칸입니다.
2타입 매개변수 T — 부를 때 채우는 칸
함수 이름 뒤의 <T>는 이 함수에 T라는 타입 칸이 있다는 표시이고, T를 타입 매개변수(type parameter)라고 부릅니다. 매개변수가 부를 때 값을 받듯, T는 부를 때 타입을 받습니다. 그림의 코드에 한 줄을 더했습니다.
function first<T>(items: T[]) {
return items[0];
}
const a = first(['커피', '라테']);
const b = first([4000, 4500]);
console.log(a, b);
커피 40001번 줄에서 items의 타입은 T[], 곧 T의 배열입니다. 2번 줄은 그 원소 items[0]을 돌려주니 돌려주는 타입은 T입니다. 함수를 만들 때는 T 칸이 비어 있습니다.
4번 줄에서 검사기는 인자 ['커피', '라테']의 타입 string[]을 T[]에 맞춰 보고 T 칸을 string으로 채웁니다. 그러면 돌려주는 타입도 string이라 a는 string입니다.
5번 줄은 새로 부르는 줄이라 T 칸도 새로 채웁니다. number[]를 T[]에 맞춰 T는 number, b도 number입니다.
| 줄 | 인자 타입 | T | 돌려주는 타입 |
|---|---|---|---|
| 4 | string[] | string | string |
| 5 | number[] | number | number |
검사 규칙 7. 타입 매개변수 T는 부를 때 채워지는 타입 칸입니다. 인자에서 추론하거나 적어 준 타입으로 채우고, 채운 타입이 T가 쓰인 매개변수와 돌려주는 타입 자리에 들어갑니다.
T라는 이름은 관례일 뿐입니다. <Item>처럼 다른 이름을 써도 되고, 그 이름이 쓰인 자리를 모두 같은 칸으로 읽습니다.
부르는 줄 둘에서 돌려받은 값의 length를 찍어 봅니다. 글자에는 글자 수 length가 있고, 수에는 없습니다.
function first<T>(items: T[]) {
return items[0];
}
const p = first(['커피', '라테']);
console.log(p.length);
const q = first([4000, 4500]);
console.log(q.length);
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
main.ts(7,15): error TS2339: Property 'length' does not exist on type 'number'.2
undefined예상과 다르면 그곳이 배울 곳입니다. 7번 줄이 막힙니다. 6번 줄에서 T 칸을 number로 새로 채워 q는 number이고, 오류 문장은 'number 타입에 length 속성이 없다'는 뜻입니다.
5번 줄은 지나갑니다. p는 string이고 '커피'는 두 글자라 2가 찍힙니다. 7번 줄은 실행하면 수 4000에서 없는 속성을 꺼내 undefined가 찍힙니다.
7번 줄도 지나간다고 봤다면 T를 첫 부르기의 string으로 본 것입니다(T가 한 번 정해진다는 생각).
3채울 타입을 적어 줄 때
T 칸은 인자에서 추론하지 않고 직접 적어 채울 수도 있습니다. 부르는 줄의 함수 이름 뒤에 <number>처럼 채울 타입을 적습니다.
function first<T>(items: T[]) {
return items[0];
}
const b = first<number>([4000]);
const a =
first<number>(['커피']);
console.log(a, b);
main.ts(6,18): error TS2322: Type 'string' is not assignable to type 'number'.커피 40005~6번 줄은 한 문장을 폰 화면에 맞추려고 = 뒤에서 나눈 것입니다.
4번 줄은 적어 준 number가 T 칸을 채워 매개변수 칸이 number[]가 됩니다. 인자 [4000]이 맞으니 지나갑니다.
6번 줄도 적어 준 number가 먼저 T 칸을 채웁니다. 그다음 인자를 number[]에 맞춰 보는데 '커피'는 글자라 막힙니다 — 'string 타입은 number 타입에 넣을 수 없다'. 인자로 T 칸을 다시 채우지 않습니다.
보통은 적지 않고 추론에 맡깁니다. 인자를 보면 T가 정해지기 때문입니다. 인자로는 알 수 없을 때 적습니다 — 빈 Map을 만들며 new Map<string, number>()처럼 적는 것은 제네릭 타입 — Array·Map·내 타입에서 봅니다.
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
마지막 원소를 꺼내는 last를 메뉴 이름에도 가격에도 씁니다. 주석 1 · 3은 제네릭 함수로 감싸는 차례입니다. 그 사이 차례 2 '쓰는 속성은 extends로'는 원소의 속성을 쓸 때 필요해서 다음 레슨에서 붙입니다.
// 1. 바뀌는 타입을 T로
function last<T>(items: T[]) {
const i = items.length - 1;
// 3. 돌려주는 값은 T로
return items[i];
}
const drinks = ['커피', '녹차'];
const prices = [4000, 3500];
const drink = last(drinks);
const won = last(prices);
console.log(drink, won + 500);
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
녹차 4000예상과 다르면 그곳이 배울 곳입니다. 검사는 오류 없이 지나갑니다. 9번 줄은 drinks의 타입 string[]으로 T 칸을 채워 drink가 string이고, 10번 줄은 T 칸을 number로 새로 채워 won이 number입니다. 그래서 11번 줄의 won + 500은 수 셈입니다.
실행하면 마지막 원소 '녹차'와, 3500에 500을 더한 4000이 찍힙니다.
10번 줄에서 막히리라 봤다면 T를 9번 줄의 string으로 고정해 본 것입니다(T가 한 번 정해진다는 생각).
3번 줄의 items.length는 원소가 아니라 배열의 속성이라 T가 무엇이든 쓸 수 있습니다. 원소의 속성을 쓰는 함수는 제약 — extends로 칸에 조건 달기에서 만듭니다.
자주 틀리는 곳
틀린 생각: 여러 타입을 받는 함수는 매개변수를 any로 적으면 된다.
글자 배열도 수 배열도 받으니, 1절의 막힘이 any 하나로 풀리는 것처럼 보입니다.
function first(items: any[]) {
return items[0];
}
const c = first([4000, 4500]);
console.log(c.toUpperCase());
main.ts:5console.log(c.toUpperCase()); ^ TypeError: c.toUpperCase is not a function at main.ts:5:15any[]의 원소를 돌려주니 c도 any이고, 검사기는 5번 줄의 toUpperCase()를 묻지 않습니다. 실행하면 수 4000에는 그런 메서드가 없어 멈춥니다.
1번 줄을 2절처럼 first<T>(items: T[])로 바꾸면 c는 number라, 검사기가 5번 줄을 TS2339로 막습니다.
맞는 규칙: any는 받은 값과 돌려주는 값의 검사를 끕니다(검사 규칙 6). 여러 타입을 받으면서 돌려주는 타입을 지키려면 타입 매개변수 T를 씁니다(검사 규칙 7).
틀린 생각: T는 처음 부를 때 정해지면 다음 부르기에서도 같다.
함수 머리의 T는 하나뿐이라, 한 번 채우면 그대로 남을 것 같습니다.
맞는 규칙: T 칸은 부르는 줄마다 새로 채웁니다. 같은 first도 4번 줄에서는 string, 5번 줄에서는 number로 채웠습니다(2절 · 검사 규칙 7).
틀린 생각: 제네릭 함수는 부를 때마다 <string>처럼 채울 타입을 적어야 한다.
선언에 <T>가 있으니 부르는 줄에도 적어야 할 것 같습니다.
맞는 규칙: 적지 않으면 검사기가 인자에서 추론해 T 칸을 채웁니다. 적어 주면 그 타입이 먼저 채우고, 인자를 그 타입에 맞춰 봅니다(3절 · 검사 규칙 7).
틀린 생각: 값이 T인지 instanceof T로 실행할 때 확인할 수 있다.
T가 타입 칸이니, 실행 중에 이 값이 T인지 물을 수 있을 것 같습니다.
function isT<T>(v: unknown) {
return v instanceof T;
}
console.log(isT<string>('라테'));
main.ts(2,23): error TS2693: 'T' only refers to a type, but is being used as a value here.main.ts:2 return v instanceof T; ^ ReferenceError: T is not defined at isT (main.ts:2:23) at main.ts:4:13오류 문장은 'T는 타입을 가리킬 뿐인데 여기서 값으로 쓰였다'는 뜻입니다. T도 다른 타입처럼 실행 전에 지워집니다(타입은 실행 전에 지워진다). 그래서 Node에는 T라는 이름이 없어 ReferenceError로 멈춥니다.
맞는 규칙: T는 검사할 때만 있는 타입 칸이라 실행할 때 물을 수 없습니다. instanceof 오른쪽에는 실행할 때도 남는 클래스(Error 등)만 씁니다(검사 규칙 1).
확인 문제
1. 다음 4~6번 줄에서 검사기는 T 칸을 무엇으로 채울까요? x · y · z의 타입을 적어 보세요.
function first<T>(items: T[]) {
return items[0];
}
const x = first([true, false]);
const y = first([[1, 2], [3]]);
const z = first(['라테', 4500]);
console.log(x, y, z);
힌트
인자 배열의 타입을 먼저 적고 T[]와 맞춰 봅니다. 원소가 배열인 [[1, 2], [3]]의 타입은 number[][]입니다.
정답
검사는 오류 없이 지나갑니다. 검사기가 채운 T와 돌려주는 타입은 이렇습니다.
| 이름 | 인자 타입 | T · 돌려주는 타입 |
|---|---|---|
| x | boolean[] | boolean |
| y | number[][] | number[] |
| z | (string | number)[] | string | number |
실행하면 배열마다 첫 원소가 찍힙니다.
true [ 1, 2 ] 라테y는 원소 하나하나가 number[]라 T가 number[]이고, 첫 원소 [1, 2]를 돌려줍니다. z는 글자와 수를 섞은 배열이라 원소 타입이 string | number입니다(배열과 튜플).
흔한 오답은 y를 number[][]로 적는 것입니다. 인자의 타입을 그대로 T로 본 것입니다(인자 타입이 곧 T라는 생각). T는 인자 타입을 T[]에 맞춰 본 원소 자리의 타입입니다.
2. i번째 원소를 꺼내는 함수 pick을 가격 배열에도 메뉴 이름 배열에도 씁니다. 9번 줄의 won이 number, 10번 줄의 tea가 string이 되도록 2번 줄의 빈칸에 타입을 채우세요.
function pick<T>(
items: ____,
i: number,
) {
return items[i];
}
const prices = [4000, 3500];
const drinks = ['커피', '녹차'];
const won = pick(prices, 1);
const tea = pick(drinks, 1);
console.log(won + 500, tea);
힌트
T 칸은 인자를 맞춰 보는 매개변수 자리에 T가 쓰여야 채워집니다. 9번 줄의 인자 prices는 number[]입니다. 이 인자에서 T가 number로 채워지려면 매개변수가 어떤 꼴이어야 할지 봅니다.
정답
빈칸은 T[]입니다. 9번 줄은 number[]를 T[]에 맞춰 T가 number, 10번 줄은 T가 string입니다. 돌려주는 값 items[i]가 T라 won은 number, tea는 string이고, 실행하면 4000 녹차가 찍힙니다.
다르게 써도 되는 곳: 1번 줄의 T를 Item처럼 다른 이름으로 바꾸면 빈칸도 Item[]입니다. 빈칸을 readonly T[]로 적어도 지나갑니다 — pick은 배열을 바꾸지 않습니다.
흔한 오답은 셋입니다.
any[]— 검사는 지나가지만 won과 tea가 any라 검사가 꺼집니다(any가 무난하다는 생각).(string | number)[]— 둘 다 받지만 won이string | number라 11번 줄의won + 500이 TS2365로 막힙니다.T— T에 무엇이 올지 몰라 items가 배열인지 알 수 없으니, 5번 줄의items[i]가 TS7053으로 막힙니다.
3. 하루 걸음 수 목록에서 첫날 걸음에 1000을 더하려는 코드가 5번 줄에서 막혔습니다. 검사 오류를 없애고, steps가 number가 되어 실행하면 9000이 찍히게 하는 고치기를 고르세요.
function first<T>(items: T[]) {
return items[0];
}
const steps =
first<string>([8000, 6500]);
console.log(steps + 1000);
main.ts(5,18): error TS2322: Type 'number' is not assignable to type 'string'.main.ts(5,24): error TS2322: Type 'number' is not assignable to type 'string'.- ① 5번 줄의
<string>을 지운다. - ② 5번 줄의
<string>을<any>로 바꾼다. - ③ 5번 줄의 인자를
['8000', '6500']으로 바꾼다.
힌트
고친 코드마다 T 칸이 무엇으로 채워지는지 정하고, 6번 줄의 steps + 1000이 수 셈인지 글자 잇기인지 봅니다.
정답
①입니다. 적어 준 타입이 없으면 검사기가 인자 number[]에서 T를 number로 채워 steps는 number이고, 실행하면 9000이 찍힙니다.
- ② 검사는 지나가지만 T를 any로 채워 steps가 number가 아니라 any가 됩니다. 실행 결과는 9000이어도 steps의 검사를 끈 것입니다(any가 무난하다는 생각).
- ③ 검사는 지나가지만 steps가 글자 '8000'이 되어, 6번 줄이 1000을 이어 붙여
80001000이 찍힙니다. 검사기가 짚은 곳은 인자지만, 고칠 곳은 잘못 적은<string>이었습니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절 첫 코드(a와 b를 찍는 코드)를 main.ts로 저장하고, 4번 줄의 first 뒤에 <number>를 적습니다. 오류가 몇 번 줄 몇 열에 몇 개 날지와 Node가 찍을 값을 먼저 적고, npx tsc로 검사한 뒤 node main.ts로 돌려 맞춰 봅니다. 편집기에서 a에 마우스를 올려 a의 타입도 확인합니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.