확인 함수(타입 가드, v is Order)는 unknown 값이 내가 정한 interface 모양인지 코드로 직접 확인해 좁히는 함수입니다. 함수가 true를 돌려준 갈래 안에서 검사기는 넘긴 값의 지금 타입을 Order로 좁힙니다.
unknown — 확인하고 쓰기에서 typeof는 원시 타입만 가려 주고, instanceof는 실행할 때 남는 클래스(Error)만 확인할 수 있었습니다. interface Order는 실행 전에 지워지므로 객체 속성은 자바스크립트 식(typeof · !== null · 'item' in v)을 이어 확인해야 합니다.
JS 다시 보기: 'item' in v는 객체 v에 item 속성이 있으면 true를 돌려줍니다. typeof null은 'object'이므로 객체인지 볼 때는 v !== null도 함께 확인합니다(JS '객체: 이름 붙은 칸 묶음' · JS '수·글자·불리언과 typeof').
1확인 함수 안 — 줄마다 좁혀지는 v
주문을 나타내는 Order와, 받은 값 v가 주문인지 확인하는 함수 isOrder입니다. 먼저 그림의 장면 A처럼 item 속성까지만 확인하는 코드를 봅시다.
interface Order {
item: string;
qty: number;
}
function isOrder(
v: unknown,
): v is Order {
return (
typeof v === 'object' &&
v !== null &&
'item' in v &&
typeof v.item === 'string'
);
}
7번 줄의 반환 타입 : v is Order는 '이 함수가 true를 돌려주면 인자로 넘긴 v의 타입은 Order이다'라고 검사기에게 알려 주는 표기입니다. 실행 때 돌려주는 값은 true 또는 false입니다.
9~12번 줄의 &&(그리고)는 앞 식이 참일 때만 뒤 식을 읽습니다. 그래서 줄을 내려갈 때마다 검사기가 보는 v의 지금 타입이 한 단계씩 좁혀집니다.
| 줄 | 식 | 그 줄에서 v의 지금 타입 |
|---|---|---|
| 9 | typeof v === 'object' | unknown |
| 10 | v !== null | object | null |
| 11 | 'item' in v | object |
| 12 | typeof v.item === 'string' | object & Record<"item", unknown> |
12번 줄의 Record<"item", unknown>이 붙은 타입은 '속성 item이 있다는 것만 확인된 객체(그 값의 타입은 아직 unknown)'라는 뜻입니다(&와 Record는 단원 8에서 배웁니다).
만약 11번 줄의 'item' in v를 빼먹고 바로 v.item을 읽으면 검사기가 막습니다.
function hasItem(v: unknown) {
return (
typeof v === 'object' &&
v !== null &&
typeof v.item === 'string'
);
}
main.ts(5,14): error TS2339: Property 'item' does not exist on type 'object'.v !== null까지만 확인한 object는 'null이 아닌 어떤 객체'일 뿐이라 item 속성이 있는지 알 수 없습니다. 속성을 꺼내기 전에 'item' in v가 먼저 와야 합니다.
2확인 함수 바깥 — v is Order는 검사기가 그대로 믿는다
1절의 isOrder는 item만 확인하고 qty는 확인하지 않았습니다. 이 함수로 qty가 빠진 JSON 글을 확인하면 어떻게 될까요? 그림의 장면 B 코드입니다.
interface Order {
item: string;
qty: number;
}
function isOrder(
v: unknown,
): v is Order {
return (
typeof v === 'object' &&
v !== null &&
'item' in v &&
typeof v.item === 'string'
);
}
앞 코드에 이어 qty가 없는 글을 넣고 isOrder로 확인합니다.
const data: unknown =
JSON.parse('{"item":"라테"}');
if (isOrder(data)) {
console.log(data.qty + 1);
}
NaN17번 줄 if (isOrder(data))의 참 갈래로 들어가면 검사기는 반환 타입 : v is Order만 보고 data의 지금 타입을 Order로 바꿉니다.
| 줄 | data의 지금 타입 |
|---|---|
| 15 | unknown |
| 17 | unknown → 참 Order 거짓 unknown |
| 18 | Order (data.qty는 number) |
검사기는 isOrder 안의 조건식이 Order의 모든 속성을 확인했는지 맞춰 보지 않습니다. 그래서 18번 줄 data.qty + 1도 통과시킵니다.
실행하면 isOrder(data)는 true를 돌려주지만 글에 qty가 없어 undefined + 1인 NaN이 찍힙니다.
이로써 단원 6의 검사 규칙 6이 완성됩니다.
검사 규칙 6. 검사기는 코드에 쓴 것만 믿습니다. 바깥에서 온 값은 검사기도 몰라
JSON.parse는 any를 줍니다.any는 검사를 끄고,unknown은 확인해 좁히기 전에는 쓰지 못하게 막습니다.as 타입·!·확인 함수의: v is 타입은 검사기에게 그 타입이라고 믿으라고 말하며, 확인 함수 안의 조건이 빠짐없는지는 사람이 챙겨야 합니다.
3빠짐없이 확인하는 isOrder
Order의 두 속성 item과 qty를 모두 확인하도록 isOrder에 'qty' in v와 typeof v.qty === 'number'를 더합니다.
Order(1절과 같음)
interface Order {
item: string;
qty: number;
}function isOrder(
v: unknown,
): v is Order {
return (
typeof v === 'object' &&
v !== null &&
'item' in v &&
'qty' in v &&
typeof v.item === 'string'
&& typeof v.qty === 'number'
);
}
앞 코드에 이어 두 개의 JSON 글을 차례로 확인해 봅니다.
for (const s of [
'{"item":"라테"}',
'{"item":"라테","qty":2}',
]) {
const v: unknown =
JSON.parse(s);
if (isOrder(v)) {
console.log(v.qty + 1);
}
}
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
3예상과 다르면 그곳이 배울 곳입니다. 검사는 오류 없이 지나갑니다.
실행하면 첫째 글 '{"item":"라테"}'는 'qty' in v에서 false가 되어 건너뛰고, 둘째 글 '{"item":"라테","qty":2}'만 if 안으로 들어가 3을 찍습니다.
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
함수 안의 확인 식은 똑같은데, 6번 줄의 반환 타입을 : v is Menu 대신 : boolean으로 적은 코드입니다.
interface Menu {
name: string;
}
function isMenu(
v: unknown,
): boolean {
return (
typeof v === 'object' &&
v !== null &&
'name' in v &&
typeof v.name === 'string'
);
}
const m: unknown =
JSON.parse('{"name":"라테"}');
if (isMenu(m)) {
console.log(m.name);
}
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
main.ts(17,15): error TS18046: 'm' is of type 'unknown'.라테예상과 다르면 그곳이 배울 곳입니다. 검사는 17번 줄에서 TS18046으로 막히고, 실행하면 라테가 찍힙니다.
반환 타입을 : boolean으로 적으면 검사기는 isMenu를 '참이나 거짓을 돌려주는 함수'로만 알 뿐, 참일 때 m이 Menu라는 뜻은 알지 못합니다. 6번 줄을 : v is Menu로 적어야 17번 줄에서 m이 Menu로 좁혀집니다.
자주 틀리는 곳
틀린 생각: 반환 타입에 v is Order를 적으면 검사기가 함수 안의 조건식이 맞는지도 확인해 준다.
v is Order라고 적었으니 속성을 하나 빼먹으면 검사기가 알려 줄 것 같습니다.
맞는 규칙: 검사기는 v is Order라는 말을 그대로 믿습니다(2절 · 검사 규칙 6). 속성을 빠짐없이 확인했는지는 사람이 대조합니다.
틀린 생각: typeof v === 'object'만 확인하면 바로 'item' in v를 쓸 수 있다.
typeof null도 'object'라서 typeof v === 'object'만 지난 v의 지금 타입은 object | null입니다.
function isObj(v: unknown) {
return (
typeof v === 'object' &&
'item' in v
);
}
main.ts(4,15): error TS18047: 'v' is possibly 'null'.맞는 규칙: 'item' in v 앞에는 반드시 typeof v === 'object'와 v !== null 두 조건이 모두 있어야 합니다(1절).
틀린 생각: 확인 함수를 따로 만들지 않고 if (data instanceof Order)로 확인하면 된다.
interface Order는 실행 전에 지워지는 타입 이름이라 instanceof 오른쪽에 쓰면 TS2693으로 막힙니다(unknown — 확인하고 쓰기 자주 틀리는 곳).
맞는 규칙: interface 모양은 instanceof로 확인할 수 없으므로 v is Order 확인 함수를 만들어 확인합니다.
틀린 생각: 확인 함수는 실행할 때 : v is Order의 Order 타입을 읽어 값을 맞춰 본다.
: v is Order가 함수 머리에 적혀 있으니 실행할 때도 타입스크립트가 Order를 보고 비교해 줄 것 같습니다.
맞는 규칙: : v is Order도 타입 표기라 실행 전에 빈칸으로 지워집니다(검사 규칙 1). 실행 때 도는 것은 함수 안에 적은 자바스크립트 식(typeof · !== null · 'item' in v)뿐입니다.
확인 문제
1. 다음 코드는 검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
interface Item {
price: number;
}
function isItem(
v: unknown,
): v is Item {
return true;
}
const d: unknown = '라테';
if (isItem(d)) {
console.log(d.price + 500);
}
힌트
isItem 안이 아무것도 확인하지 않고 return true;만 할 때, 검사기가 10~11번 줄을 어떻게 보는지와 실제 d에 들어 있는 값을 나눠 봅니다.
정답
NaN검사는 오류 없이 지나가고, 실행하면 NaN이 찍힙니다.
검사기는 : v is Item만 믿고 11번 줄의 d를 Item으로 좁힙니다(검사 규칙 6). 실행하면 isItem('라테')가 true를 돌려주어 11번 줄로 들어가고, 글자 '라테'에는 price가 없어 undefined + 500인 NaN이 됩니다.
흔한 오답은 '7번 줄에서 검사 오류가 난다'입니다. 검사기가 확인 함수 안의 논리까지 검증해 준다고 본 것입니다.
2. 아래 코드가 검사를 지나고 실행하면 라테를 찍도록 6번 줄의 빈칸에 알맞은 반환 타입을 채우세요.
interface Menu {
name: string;
}
function isMenu(
v: unknown,
): ____ {
return (
typeof v === 'object' &&
v !== null &&
'name' in v &&
typeof v.name === 'string'
);
}
const m: unknown =
JSON.parse('{"name":"라테"}');
if (isMenu(m)) {
console.log(m.name);
}
힌트
함수가 true를 돌려줄 때 매개변수 v가 Menu임을 검사기에게 알려 주는 세 단어 표기입니다(1절 · 예제).
정답
빈칸은 v is Menu입니다. if (isMenu(m))의 참 갈래 안에서 m의 지금 타입이 Menu로 좁혀져 m.name을 읽을 수 있습니다.
흔한 오답은 boolean입니다. boolean으로 적으면 17번 줄에서 m이 그대로 unknown이라 TS18046으로 막힙니다(예제).
3. function check(v: unknown) 안에서 typeof v === 'object'와 v !== null, 그리고 'qty' in v까지 통과한 직후, 다음 식에서 검사기가 보는 v의 지금 타입을 고르세요.
- ①
object & Record<"qty", unknown> - ②
{ qty: number } - ③
object | null
힌트
'qty' in v는 객체 v 안에 qty라는 키가 있다는 것만 확인했을 뿐, 그 값의 타입이 number인지는 아직 확인하기 전입니다(1절 표).
정답
①입니다. typeof v === 'object'로 object | null이 되고, v !== null로 object가 된 뒤, 'qty' in v를 지나면 qty 속성이 있다는 뜻의 Record<"qty", unknown>이 더해진 타입이 됩니다.
- ②
typeof v.qty === 'number'까지 확인하고: v is ...함수를 거치기 전에는qty의 타입은 아직unknown입니다. - ③
v !== null을 이미 지났으므로null은 빠졌습니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 코드의 11번 줄 'item' in v &&를 지우고 npx tsc로 검사해 TS2339가 나는지 확인해 보세요. 다음 레슨은 바깥 값 확인하기입니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.