never는 값이 하나도 들지 않은 빈 모음입니다. 판별 유니언을 switch로 나눌 때 default에서 남은 갈래를 never 자리에 넣어 두면, 갈래가 새로 생겼는데 맡는 case가 없을 때 검사기가 그 줄에서 빠진 갈래의 이름을 알려 줍니다.
카페에 기프트 카드 주문(Gift)이 새로 생겼다고 합시다. Order에 Gift를 더하면, 주문을 갈래마다 나누던 함수마다 Gift를 맡는 case가 있어야 합니다.
그런데 판별 유니언의 자주 틀리는 곳에서 본 것처럼, case를 빠뜨려도 그 switch에는 검사 오류가 없습니다. 고칠 함수를 사람이 하나씩 찾아야 합니다.
이 레슨은 그 일을 검사기에게 맡기는 틀을 배웁니다. 하위 목표는 셋입니다 — 갈래 속성으로 나누기, 갈래마다 처리, 나머지는 never로.
1never — 값이 하나도 없는 모음
타입은 그 자리에 올 수 있는 값의 모음입니다(검사 규칙 2). never는 아무 값도 들지 않은 모음입니다. 그래서 never 자리에는 어떤 값도 넣을 수 없습니다.
let x: never = 0;
main.ts(1,5): error TS2322: Type '0' is not assignable to type 'never'.Type '0'은 '0 타입', is not assignable to는 '…에 넣을 수 없다', type 'never'는 'never 타입'입니다. 곧 never 자리에 값 0을 넣었다는 뜻입니다. 오류 문장의 '0'은 글자가 아니라 값 0 하나짜리 타입입니다. 글자든 객체든 어떤 값을 넣어도 같은 TS2322가 납니다.
거꾸로 빈 모음은 어느 모음에도 들어갑니다. 들어갈 값이 하나도 없으니 걸릴 것이 없습니다. 지금 타입이 never인 이름은 never 자리에도, number 자리에도 들어갑니다.
지금 타입이 never인 이름은 갈래를 확인해 나가다가 남은 갈래가 하나도 없을 때 생깁니다.
2나머지는 never로 — default에 남은 갈래
판별 유니언 3절과 같은 타입 선언에서 시작합니다.
interface Drink {
kind: 'drink';
price: number;
extra: number;
}
interface Food {
kind: 'food';
price: number;
}
type Order = Drink | Food;
앞 코드에 이어 그 레슨 3절의 cost에 default를 더해 씁니다. 검사 오류가 없습니다.
function cost(o: Order) {
switch (o.kind) {
case 'drink':
return o.price + o.extra;
case 'food':
return o.price;
default:
const left: never = o;
return left;
}
}
12번 줄이 갈래 속성으로 나누고(하위 목표 1), 13번 줄과 15번 줄의 case가 Drink와 Food를 맡습니다(하위 목표 2).
default에는 위 case가 맡지 않고 남은 갈래가 옵니다. 여기서는 남은 갈래가 없어, 17번 줄에서 o의 지금 타입은 never입니다(하위 목표 3).
| 줄 | 이름 | 지금 타입 |
|---|---|---|
| 11 | o | Order |
| 13 | o | case 'drink' 갈래 안 Drink |
| 15 | o | case 'food' 갈래 안 Food |
| 17 | o | default 갈래 안 never — 남은 갈래 없음 |
18번 줄은 남은 것(left)을 never 자리에 넣습니다. o가 never라 지나갑니다.
19번 줄 return left;는 이 갈래도 값을 돌려주는 꼴로 끝냅니다. 이 줄을 빼면 default를 지나 함수 끝에 닿는 길이 생겨, cost가 돌려주는 타입에 undefined가 더해집니다(number | undefined). left는 never라 돌려줘도 돌려주는 타입은 number 그대로입니다.
3새 갈래가 생기면 never 줄이 알려 준다
음료와 음식의 타입은 그대로 둡니다.
interface Drink {
kind: 'drink';
price: number;
extra: number;
}
interface Food {
kind: 'food';
price: number;
}
앞 코드에 이어 Gift를 더하고, Order를 갈래 셋으로 고칩니다.
interface Gift {
kind: 'gift';
price: number;
}
type Order =
| Drink
| Food
| Gift;
앞 코드에 이어 2절의 cost를 한 글자도 바꾸지 않고 씁니다. 이 코드는 검사를 지날까요? 막힌다면 몇 번 줄일까요?
function cost(o: Order) {
switch (o.kind) {
case 'drink':
return o.price + o.extra;
case 'food':
return o.price;
default:
const left: never = o;
return left;
}
}
검사 결과 보기
main.ts(25,13): error TS2322: Type 'Gift' is not assignable to type 'never'.예상과 다르면 그곳이 배울 곳입니다. 20번 줄과 22번 줄의 case가 Drink와 Food를 맡고, Gift를 맡는 case는 없습니다. 그래서 24번 줄 default에 Gift가 남아 o의 지금 타입이 Gift입니다.
25번 줄은 Gift를 never 자리에 넣으려다 막힙니다. Type 'Gift'는 'Gift 타입', is not assignable to는 '…에 넣을 수 없다'입니다. 곧 맡지 않은 갈래 Gift가 남아 never 자리에 들어가지 못했다는 뜻입니다. 빠진 갈래의 이름이 오류 문장에 나옵니다.
검사기는 값을 모르니 실행이 default로 갈지 따지지 않고, 실행되지 않을 줄이라도 그 아래 줄까지 읽습니다(검사 규칙 5).
고칠 곳은 25번 줄이 아니라 case가 빠진 switch입니다. 23번 줄 뒤에 case 'gift':와 return o.price; 두 줄을 더하면 default의 o는 다시 never가 되고 검사 오류가 없습니다.
Order를 쓰는 함수가 여럿이어도, never 줄을 둔 함수마다 같은 오류가 나서 고칠 곳을 빠짐없이 알려 줍니다.
예제
하위 목표 주석을 단 풀이 예제
2절 코드의 cost에 하위 목표마다 주석 머리줄을 달았습니다. Order를 갈래마다 나누는 함수는 이 꼴로 씁니다.
타입 선언(2절과 같음)
interface Drink {
kind: 'drink';
price: number;
extra: number;
}
interface Food {
kind: 'food';
price: number;
}
type Order = Drink | Food;2절과 같은 타입 선언에 이어 주석을 단 cost를 씁니다.
function cost(o: Order) {
// 1. 갈래 속성으로 나누기
switch (o.kind) {
// 2. 갈래마다 처리
case 'drink':
return o.price + o.extra;
case 'food':
return o.price;
// 3. 나머지는 never로
default:
const left: never = o;
return left;
}
}
갈래가 늘면 2번 하위 목표에 case를 하나 더 씁니다. 1번과 3번은 갈래가 몇 개든 같은 꼴입니다.
자주 틀리는 곳
틀린 생각: never는 값을 돌려주지 않는 함수에만 쓰는 타입이다.
끝나지 않거나 늘 오류로 멈추는 함수에 적는 타입으로 never를 먼저 만나면 그렇게 여기기 쉽습니다. 그 함수에 붙은 never도 '돌려받을 값이 하나도 없다'는 뜻이라, 빈 모음과 같은 말입니다.
이 레슨의 never는 갈래를 다 걸러 낸 뒤에 남은 것이 없다는 지금 타입입니다. 1절의 x처럼 이름에 적는 타입이기도 합니다.
맞는 규칙: never는 값이 하나도 없는 모음이라 어떤 값도 들어가지 못합니다(검사 규칙 2). 그래서 남은 갈래가 없어야 하는 자리에 적어 두고 검사기에게 확인을 맡깁니다.
틀린 생각: default를 두지 않아도, 새 갈래가 빠지면 어디선가 검사 오류가 난다.
default 없이 Gift만 더한 cost에는 검사 오류가 없습니다. 기프트 카드 주문은 switch를 지나 함수 끝에 닿고, 실행하면 undefined를 돌려줍니다. cost가 돌려주는 타입에 undefined가 더해질 뿐입니다.
금액을 수로 쓰는 줄 total + cost(o)에서야 TS2532(undefined일 수 있다)가 납니다. 이 오류는 Gift라는 이름도, 고칠 switch도 말해 주지 않습니다. 금액을 console.log로 찍기만 하면 그마저 없이 undefined가 찍힙니다.
맞는 규칙: 맡는 case가 없는 갈래는 오류 없이 switch를 지나갑니다. 빠진 갈래를 그 자리에서 알리려면 default에 never 줄을 둡니다.
틀린 생각: never 줄의 오류는 지워야 할 걸림돌이다.
오류를 없애는 법을 검색하면 as로 덮는 길이 나오지만, 덮어도 Gift를 맡는 case는 없어 빠진 갈래가 그대로 남습니다(6단원).
맞는 규칙: never 줄의 오류는 빠진 갈래를 알려 주는 정보입니다. 고칠 곳은 never 줄이 아니라 case가 빠진 switch이고, 그 갈래를 맡는 case를 더합니다.
확인 문제
1. 2절 코드에서 case 'food': 두 줄을 지웠습니다. 타입 선언은 2절과 같습니다. 검사를 지날까요? 막힌다면 몇 번 줄에서, 오류 문장에 어느 갈래 이름이 나올까요?
interface Drink {
kind: 'drink';
price: number;
extra: number;
}
interface Food {
kind: 'food';
price: number;
}
type Order = Drink | Food;
앞 코드에 이어 두 줄을 지운 cost를 씁니다.
function cost(o: Order) {
switch (o.kind) {
case 'drink':
return o.price + o.extra;
default:
const left: never = o;
return left;
}
}
힌트
15번 줄 default에 어느 갈래가 남는지 봅니다.
정답
main.ts(16,13): error TS2322: Type 'Food' is not assignable to type 'never'.16번 줄에서 막히고, 오류 문장에 Food가 나옵니다. case 'drink'만 남아 default에 Food가 남고, Food는 never 자리에 들어가지 못합니다.
흔한 오답은 '지나간다'입니다. 판별 유니언 레슨처럼 case를 빠뜨려도 조용하다고 본 것입니다. 이번에는 default에 never 줄이 있어, 남은 Food가 그 줄에서 걸립니다.
2. 갈래마다 처리하기 틀을 씁니다.
(가) 2절 코드의 18번 줄을 const left: ____ = o;로 비웠습니다. 빈칸에 들어갈 타입을 쓰세요.
힌트
3절처럼 Gift를 더했을 때 이 줄이 막혀야 빠진 갈래를 알 수 있습니다. 어떤 타입 자리여야 Gift가 들어가지 못할지 봅니다.
정답
never입니다.
흔한 오답은 Order입니다. 검사는 지나지만, Gift를 더해도 Gift가 Order 자리에 들어가 아무 오류도 나지 않습니다. 빠진 갈래를 알려 주는 일은 빈 모음 never만 합니다.
(나) 주문 갈래의 이름을 돌려주는 함수입니다. 3번 하위 목표를 통째로 채우세요. 다 채운 코드는 검사 오류가 없어야 하고, Order에 Gift를 더하면 3번 하위 목표 안에서 막혀야 합니다.
function label(o: Order) {
// 1. 갈래 속성으로 나누기
switch (o.kind) {
// 2. 갈래마다 처리
case 'drink':
return '음료';
case 'food':
return '음식';
// 3. 나머지는 never로
}
}
힌트
2절 코드의 17~19번 줄과 같은 세 줄입니다. 이름 o는 그대로 씁니다.
정답
2절과 같은 타입 선언 아래에 채운 label을 두면 검사 오류가 없습니다.
interface Drink {
kind: 'drink';
price: number;
extra: number;
}
interface Food {
kind: 'food';
price: number;
}
type Order = Drink | Food;앞 코드에 이어 채운 label을 씁니다.
function label(o: Order) {
// 1. 갈래 속성으로 나누기
switch (o.kind) {
// 2. 갈래마다 처리
case 'drink':
return '음료';
case 'food':
return '음식';
// 3. 나머지는 never로
default:
const left: never = o;
return left;
}
}Order에 Gift를 더하면 const left: never = o; 줄에서 3절과 같은 TS2322로 막힙니다.
다르게 써도 되는 곳: 남은 것의 이름은 left가 아니어도 됩니다(other · none). = o의 o는 바꿀 수 없습니다 — 남은 갈래가 든 이름이 o이기 때문입니다.
흔한 오답은 return left;를 빠뜨리는 것입니다. 검사 오류는 없지만, label이 돌려주는 타입에 undefined가 더해집니다.
3. 성적을 학점으로 바꾸는 코드입니다. A는 4, B는 3, C는 2입니다. 이 코드의 검사 결과를 보고, 빠진 곳을 알맞게 고치는 길을 고르세요.
type Grade = 'A' | 'B' | 'C';
function point(g: Grade) {
switch (g) {
case 'A':
return 4;
case 'B':
return 3;
default:
const left: never = g;
return left;
}
}
main.ts(9,13): error TS2322: Type '"C"' is not assignable to type 'never'.- ① 7번 줄 뒤에
case 'C':와return 2;를 더한다. - ② 9번 줄의
never를Grade로 바꾼다. - ③ 1번 줄의
Grade에서'C'를 지운다.
힌트
오류 문장이 어느 값이 남았다고 하는지 봅니다. 고친 뒤 point('C')가 무엇을 돌려줄지도 따져 봅니다.
정답
①입니다. 갈래 속성 없이 리터럴 유니언 값 자체로 나눠도 같은 틀이 됩니다. 남은 값 'C'를 맡는 case를 더하면 default의 g는 never입니다.
type Grade = 'A' | 'B' | 'C';
function point(g: Grade) {
switch (g) {
case 'A':
return 4;
case 'B':
return 3;
case 'C':
return 2;
default:
const left: never = g;
return left;
}
}
console.log(point('C'));2- ② 검사는 지나지만
'C'가default로 가point('C')가 2 대신C를 돌려줍니다. 검사를 지났다고 결과가 맞는 것은 아닙니다. - ③ 검사는 지나지만 C 학점을 적을 수 없는 타입이 됩니다. 데이터가 아니라 코드를 고칠 일이었습니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절의 타입 선언과 cost를 main.ts로 저장하고 npx tsc로 검사합니다. 아무것도 찍히지 않으면 검사 오류가 없는 것입니다. 그다음 3절처럼 Gift를 더하면 어느 줄에 어떤 이름이 나올지 먼저 적고, 다시 검사해 맞춰 봅니다. 마지막으로 case 'gift':를 더해 오류가 사라지는지 봅니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.