본문으로 건너뛰기

직접 만드는 확인 함수

타입스크립트 목차바깥에서 온 값 — any·unknown·as레슨 30 / 51

목차

확인 함수(타입 가드, 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').

8단계 중 1단계. 6~7번 줄: 매개변수 v는 unknown이고, 반환 타입은 v is Order입니다.

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의 지금 타입
9typeof v === 'object'unknown
10v !== nullobject | null
11'item' in vobject
12typeof 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'
  );
}
검사 결과 오류 1개
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);
}
검사 결과 오류 없음
실행 결과
NaN

17번 줄 if (isOrder(data))의 참 갈래로 들어가면 검사기는 반환 타입 : v is Order만 보고 data의 지금 타입을 Order로 바꿉니다.

줄data의 지금 타입
15unknown
17unknown → 참 Order 거짓 unknown
18Order (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로 돌리면 무엇이 찍힐까요?

검사 결과와 실행 결과 보기
검사 결과 오류 1개
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
  );
}
검사 결과 오류 1개
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가 나는지 확인해 보세요. 다음 레슨은 바깥 값 확인하기입니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로