unknown은 모든 값을 받아 주되, 확인하기 전에는 아무것도 못 쓰게 막는 타입입니다. any는 검사를 끈다에서 본 any와 달리, typeof나 instanceof로 좁힌 뒤에만 속성을 읽거나 셈할 수 있습니다.
바깥에서 온 값에 어떤 모양이 들었는지 아직 모를 때, any 그대로 두면 오타나 잘못된 타입이 실행까지 넘어갑니다. 그 자리에 unknown을 적으면 확인을 빠뜨린 줄을 검사기가 바로 짚어 줍니다.
JS 다시 보기: throw는 Error 객체뿐 아니라 글자나 수도 던질 수 있고, e instanceof Error는 e가 Error로 만든 객체이면 true를 돌려줍니다(JS 'try와 catch' · JS 'throw로 오류 알리기' · JS '클래스와 new').
1unknown은 다 받지만 바로 못 쓴다
앞 레슨 끝 '컴퓨터에서 해 보기'처럼 JSON.parse가 돌려준 값에 : unknown을 붙이고 속성을 꺼내 봅시다.
const text =
'{"name":"라테","price":4500}';
const data: unknown =
JSON.parse(text);
console.log(data.price);
main.ts(5,13): error TS18046: 'data' is of type 'unknown'.JSON.parse가 돌려준 any가 3번 줄의 : unknown 자리로 들어가면서 data의 타입이 unknown이 되었습니다.
5번 줄은 TS18046으로 막힙니다 — 'data' is of type 'unknown', data의 타입이 unknown이라 price라는 속성이 있는지 아직 모른다는 뜻입니다.
unknown을 모음(집합)으로 그리면 string · number · 객체를 모두 감싸는 가장 큰 바깥 테두리입니다. 그래서 값을 넣을 때는 무엇이든 들어갑니다. 그러나 다른 타입 자리로 넘기거나 바로 쓰려고 하면 막힙니다.
let v: unknown = '라테';
v = 4500;
const n: number = v;
main.ts(3,7): error TS2322: Type 'unknown' is not assignable to type 'number'.1번 줄의 글자 '라테'와 2번 줄의 수 4500은 모두 unknown에 들어갑니다.
그러나 좁힘이 달라지는 곳 — 대입과 반복에서 본 유니언(string | number)과 달리, unknown은 2번 줄에서 수를 넣어도 지금 타입이 number로 좁혀지지 않고 unknown 그대로입니다.
그래서 3번 줄에서 number 자리인 n에 넣으려 하면 TS2322로 막힙니다. any였다면 3번 줄이 검사 없이 지나갔을 자리입니다.
2catch의 e는 unknown이다 — instanceof로 좁히기
try 블록 안에서 던져진 값을 받는 catch (e)의 e도 타입이 unknown입니다. 자바스크립트의 throw는 new Error('품절')뿐 아니라 글자 '품절'이나 수 404처럼 어떤 값이든 던질 수 있어서, 검사기는 e에 무엇이 올지 미리 알 수 없기 때문입니다.
그림의 장면 A처럼 catch (e) 안에서 바로 e.name을 읽으면 TS18046으로 막힙니다.
try {
JSON.parse('{');
} catch (e) {
console.log(e.name);
}
main.ts(4,15): error TS18046: 'e' is of type 'unknown'.e가 Error 객체인지 확인하려면 instanceof Error를 씁니다. 그림의 장면 B처럼 if (e instanceof Error) 갈래 안에서는 e의 지금 타입이 Error로 좁혀집니다.
try {
JSON.parse('{');
} catch (e) {
if (e instanceof Error) {
console.log(e.name);
}
}
SyntaxError4번 줄에서 e instanceof Error로 확인한 뒤 5번 줄로 들어가면, e가 바깥 unknown 테두리에서 안쪽 Error 모음으로 들어갑니다. Error에는 name과 message 속성이 있으므로 검사를 지나고, 실행하면 깨진 JSON이 던진 오류 이름 SyntaxError가 찍힙니다.
3typeof로 확인하고 쓰기
원시 타입 값인지 확인할 때는 좁히기 — typeof와 타입 추적 표에서 배운 typeof를 그대로 씁니다. 다음 코드는 JSON 글 '4500'을 읽어 500을 더합니다.
const v: unknown =
JSON.parse('4500');
if (typeof v === 'number') {
console.log(v + 500);
}
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
5000예상과 다르면 그곳이 배울 곳입니다. 1~2번 줄에서 v의 선언 타입은 unknown이지만, 3번 줄 typeof v === 'number'가 참인 갈래 안(4번 줄)에서는 지금 타입이 number로 좁혀집니다. 그래서 v + 500이 검사를 지나고, 실행하면 5000이 찍힙니다.
앞 레슨의 검사 규칙 6에 unknown을 더해 적으면 다음과 같습니다.
검사 규칙 6. 검사기는 코드에 쓴 것만 믿습니다.
any는 검사를 끄고,unknown은 모든 값을 받되typeof·instanceof로 확인해 좁히기 전에는 쓰지 못하게 막습니다.
두 타입의 차이를 한눈에 모으면 이렇습니다.
| 할 일 | any | unknown |
|---|---|---|
| 아무 값이나 넣기 | 지나감 | 지나감 |
| 확인 없이 속성 읽기 · 연산 | 지나감(검사 꺼짐) | 막힘(TS18046) |
확인 없이 number 자리에 넣기 | 지나감(검사 꺼짐) | 막힘(TS2322) |
typeof · instanceof 뒤 | 그대로 any | 그 타입으로 좁혀짐 |
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
재고가 없을 때 Error 객체 대신 글자 '품절'을 던지는 코드입니다. catch (e) 안에서 두 갈래로 확인합니다.
try {
throw '품절';
} catch (e) {
if (e instanceof Error) {
console.log(e.message);
} else if (
typeof e === 'string'
) {
console.log('알림: ' + e);
}
}
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
알림: 품절예상과 다르면 그곳이 배울 곳입니다. 검사는 오류 없이 지나갑니다. 5번 줄에서 e의 지금 타입은 Error이고, 9번 줄에서는 typeof e === 'string'으로 좁혀져 string입니다.
실행하면 던진 값 '품절'은 Error 객체가 아니라 글자이므로 첫 갈래를 건너뛰고 둘째 갈래로 들어가 알림: 품절을 찍습니다. catch의 e가 늘 Error라고 여겨 확인 없이 e.message만 썼다면 검사기가 TS18046으로 막습니다. e가 any였다면 undefined가 찍혔을 자리입니다.
자주 틀리는 곳
틀린 생각: unknown과 any는 둘 다 아무 값이나 받으니 같은 타입이다.
값을 넣는 줄만 보면 둘 다 오류 없이 지나가서 같아 보입니다. 차이는 그 값을 꺼내 쓰는 줄에서 드러납니다.
맞는 규칙: any는 검사를 꺼서 확인 없이 써도 지나가고, unknown은 확인해 좁히기 전까지 모든 쓰기를 막습니다(3절 표 · 검사 규칙 6). 바깥에서 온 값은 unknown으로 받아 확인합니다.
틀린 생각: catch (e)의 e는 늘 Error이다.
오류라고 부르니 e에는 늘 Error 객체가 들어올 것 같습니다. 그러나 자바스크립트는 throw '품절'처럼 아무 값이나 던질 수 있어서, 타입스크립트(strict)는 e를 unknown으로 둡니다.
맞는 규칙: catch (e) 안에서 e.name이나 e.message를 쓰려면 먼저 if (e instanceof Error)로 확인합니다(2절).
틀린 생각: TS18046이 나면 확인하는 대신 as를 붙여 넘기면 된다.
if로 확인하는 줄을 쓰기 번거로울 때 (data as Menu).price처럼 as를 붙여 오류를 없애는 코드를 흔히 봅니다. as는 값을 확인하지 않고 검사기에게 '이 타입이라고 믿어라' 하고 말할 뿐입니다.
맞는 규칙: as가 왜 위험한지와 값을 덮는 네 가지 길의 비교는 다음 레슨 as는 검사기에게 하는 말에서 다룹니다.
틀린 생각: instanceof로 interface 타입도 확인할 수 있다.
e instanceof Error가 되었으니, 내가 적은 interface Menu도 v instanceof Menu로 확인할 수 있을 것 같습니다.
interface Menu {
name: string;
}
const v: unknown =
JSON.parse('{"name":"라테"}');
if (v instanceof Menu) {
console.log(v.name);
}
main.ts(6,18): error TS2693: 'Menu' only refers to a type, but is being used as a value here.main.ts(7,15): error TS18046: 'v' is of type 'unknown'.Error는 실행할 때도 남아 있는 클래스(생성자)이지만, interface Menu는 타입은 실행 전에 지워진다에서 보았듯 실행 전에 지워집니다. 그래서 6번 줄은 Menu가 타입일 뿐인데 값 자리에 썼다는 뜻의 TS2693('Menu' only refers to a type)으로 막힙니다.
맞는 규칙: instanceof 오른쪽에는 실행할 때도 남아 있는 클래스(Error 등)만 쓸 수 있습니다. interface 모양인지 확인하는 함수는 직접 만드는 확인 함수에서 만듭니다.
확인 문제
1. 다음 코드는 검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
let v: unknown = '라테';
console.log(v.length);
힌트
1번 줄에서 글자 '라테'를 넣었을 때 v의 지금 타입이 string으로 좁혀지는지, 그대로 unknown인지 1절을 떠올려 봅니다.
정답
main.ts(2,13): error TS18046: 'v' is of type 'unknown'.2검사는 2번 줄에서 TS18046으로 막히고, Node로 돌리면 타입 표기 : unknown이 지워져 '라테'.length인 2가 찍힙니다.
unknown은 값을 대입해도 지금 타입이 좁혀지지 않습니다. 2번 줄이 지나가게 하려면 if (typeof v === 'string') 안에서 v.length를 읽어야 합니다.
흔한 오답은 '1번 줄에 글자를 넣었으니 검사를 지난다'입니다. 유니언(string | number)의 대입 좁히기를 unknown에도 그대로 적용한 것입니다.
2. 아래 코드가 검사를 지나고 실행하면 SyntaxError를 찍도록 4번 줄의 빈칸에 알맞은 연산자를 채우세요.
try {
JSON.parse('{');
} catch (e) {
if (e ____ Error) {
console.log(e.name);
}
}
힌트
e가 Error 클래스로 만든 객체인지 확인하는 자바스크립트 연산자입니다(2절).
정답
빈칸은 instanceof입니다. if (e instanceof Error) 갈래 안에서 e의 지금 타입이 Error로 좁혀져 e.name을 읽을 수 있습니다.
흔한 오답은 typeof입니다. Error 객체에 typeof를 쓰면 'object'가 나올 뿐 Error인지 가려 주지 못합니다.
3. 아래 코드는 3번 줄에서 TS18046으로 막힙니다. v의 : unknown을 유지하면서 검사 오류를 없애고, 실행하면 3100이 찍히게 하는 고치기를 고르세요.
const v: unknown =
JSON.parse('3000');
console.log(v + 100);
main.ts(3,13): error TS18046: 'v' is of type 'unknown'.- ① 3번 줄을
if (typeof v === 'number') { console.log(v + 100); }로 바꾼다. - ② 1번 줄의
: unknown을: any로 바꾼다. - ③ 3번 줄을
if (v instanceof number) { console.log(v + 100); }로 바꾼다.
힌트
원시 타입인 number를 실행할 때 확인하는 방법과, unknown을 쓰는 목적을 함께 생각해 봅니다.
정답
①입니다. typeof v === 'number' 갈래 안에서 v가 number로 좁혀져 검사를 지나고, 실행하면 3100이 찍힙니다.
- ②
: any로 바꾸면 검사를 꺼 버려unknown을 쓴 뜻이 사라집니다. - ③
number는 타입 이름이라 실행할 때 남지 않으므로instanceof뒤에 쓸 수 없습니다(TS2693). 원시 타입은typeof로 확인합니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
any는 검사를 끈다 3절 코드의 7번 줄 대신 order.qty를 const qty: unknown으로 받고, typeof qty === 'number'인 갈래 안에서만 addOne(qty)를 부르도록 고쳐 보세요. npx tsc로 검사하고 Node로 돌려 21이 찍히지 않는지 확인합니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.