자바스크립트 코드에 타입을 붙일 때는 모든 줄에 적지 않습니다. 하위 목표 넷(모양을 interface로 · 매개변수에 타입 · 빈 객체와 빈 배열에 타입 · 검사하며 하나씩 고치기)을 차례로 하면, 나머지 타입은 검사기가 추론합니다.
자바스크립트로 쓴 코드를 main.ts에 옮기면 검사 오류가 나고, 타입을 붙이면 실행만으로는 몰랐던 오타도 드러납니다. 이 단원에서 배운 것으로 코드를 직접 쓰는 틀 레슨입니다.
JS 다시 보기: 주문 한 건은 { item: '커피', qty: 2 }처럼 메뉴와 잔 수를 가진 객체이고, 주문 셋을 배열에 담아 orders[1].qty처럼 꺼냅니다(JS '객체를 담은 배열').
1받은 자바스크립트 코드를 먼저 검사한다
주문 하나를 '라테 1잔'처럼 쓰는 자바스크립트 코드를 고치지 않고 main.ts에 옮겨 검사하고 돌립니다.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
function label(o) {
return `${o.item} ${o.qyt}잔`;
}
console.log(label(orders[1]));
main.ts(7,16): error TS7006: Parameter 'o' implicitly has an 'any' type.라테 undefined잔TS7006은 매개변수 o의 타입이 정해지지 않아 any가 됐다는 뜻입니다. 매개변수는 추론되지 않아 적어야 합니다.
실행 결과의 undefined잔은 8번 줄의 오타 o.qyt 때문입니다. 자바스크립트는 없는 속성을 꺼내도 멈추지 않고 undefined를 줍니다.
검사기도 이 오타를 짚지 않았습니다. o가 any라 o의 속성을 맞춰 볼 수 없어서입니다. 그래서 타입을 붙이는 일은 오류를 지우는 일이면서, 검사기가 볼 수 있는 곳을 넓히는 일입니다.
| 하위 목표 | 하는 일 | 이 레슨의 코드 |
|---|---|---|
| 1. 모양을 interface로 | 객체 하나의 속성 줄을 보고 interface로 적는다 | Order |
| 2. 매개변수에 타입 | 함수 매개변수에 1의 이름을 적는다 | o: Order |
| 3. 빈 객체와 빈 배열에 타입 | 첫 값이 없어 추론할 수 없는 곳에 적는다 | sold: Sold · big: string[] |
| 4. 검사하며 하나씩 고치기 | 검사하고, 오류를 위에서부터 하나씩 고쳐 다시 검사한다 | — |
2모양을 interface로, 매개변수에 타입
객체 하나 { item: '커피', qty: 2 }를 보고 item은 string, qty는 number로 적고(하위 목표 1), 그 이름 Order를 매개변수 o에 적습니다(하위 목표 2).
// 1. 모양을 interface로
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
// 2. 매개변수에 타입
function label(o: Order) {
return `${o.item} ${o.qyt}잔`;
}
console.log(label(orders[1]));
main.ts(13,25): error TS2339: Property 'qyt' does not exist on type 'Order'.TS7006은 사라지고, 실행 결과로만 보이던 오타를 이제 검사기가 짚습니다 — 'Order 타입에 qyt 속성이 없다'. 글자 차례가 바뀐 오타라 Did you mean은 붙지 않았습니다.
orders에는 타입을 적지 않았습니다. 15번 줄에서 orders[1]을 Order 자리에 넘길 때, 검사기가 배열 안 객체에서 추론한 모양을 맞춰 봅니다(검사 규칙 4).
매개변수를 { item, qty }처럼 구조 분해로 받는다면, 타입은 }: Order)처럼 구조 분해를 닫은 뒤에 붙입니다. 구조 분해 안의 :은 이름 바꾸기입니다(이 단원의 '객체 타입과 interface').
3빈 객체와 빈 배열에 타입
JS 다시 보기: 세는 코드는 빈 객체 {}에서 시작해, 주문을 하나 꺼낼 때마다 그 메뉴 칸을 1 올립니다(JS '세기: 메뉴별로 모으기').
주문을 메뉴별 잔 수로 세는 자바스크립트 코드입니다. 고치지 않고 옮겨 검사하면 몇 번 줄이 막힐까요? 앞 레슨에서 본 오류입니다. 답을 정한 뒤 펼쳐 보세요.
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
const sold = {};
for (const o of orders) {
const n = sold[o.item] ?? 0;
sold[o.item] = n + o.qty;
}
console.log(sold);
검사 결과 보기
main.ts(8,13): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. No index signature with a parameter of type 'string' was found on type '{}'.main.ts(9,3): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. No index signature with a parameter of type 'string' was found on type '{}'.예상과 다르면 그곳이 배울 곳입니다. 칸 이름을 대괄호로 넣는 8 · 9번 줄의 TS7053입니다. 빈 객체는 줄이 없는 모양 {}라 string 칸 이름을 받지 못합니다.
실행은 검사와 따로 돌아 자바스크립트 때와 같은 결과를 찍습니다.
{ '커피': 2, '라테': 1, '쿠키': 1 }빈 객체에는 인덱스 시그니처로 모양을 적습니다.
interface Sold {
[item: string]: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
// 3. 빈 객체와 빈 배열에 타입
const sold: Sold = {};
for (const o of orders) {
const n = sold[o.item] ?? 0;
sold[o.item] = n + o.qty;
}
console.log(sold);
{ '커피': 2, '라테': 1, '쿠키': 1 }반복이 꺼낸 o는 orders에서 추론되므로 Order를 적지 않아도 됩니다.
빈 배열도 첫 값이 없어 원소 타입을 추론하지 못하니, 앞 단원 '배열과 튜플'에서 본 대로 const big: string[] = [];처럼 원소 타입을 적습니다.
4검사하며 하나씩 고치기
오류가 여럿이면 위에서부터 하나씩 고치고 다시 검사합니다. 2절처럼 숨어 있던 오류가 드러나기도 합니다.
2절 코드의 13번 줄 qyt를 qty로 고치면 검사 오류가 0입니다. 그래도 끝이 아닙니다. 자바스크립트 때와 같은 값으로 돌려, orders[1]이 바라던 라테 1잔을 찍는지 봅니다.
파일이나 JSON 글처럼 실행 중에 들어오는 값은 검사기가 미리 보지 못합니다(6단원).
예제
주문 셋을 모두 한 줄씩 쓰기
4절에서 고친 코드를 주석 없이 쓰고, 마지막 줄만 반복으로 바꿔 주문 셋을 모두 씁니다.
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
function label(o: Order) {
return `${o.item} ${o.qty}잔`;
}
for (const o of orders) {
console.log(label(o));
}
커피 2잔
라테 1잔
쿠키 1잔둘째 줄이 1절에서 라테 undefined잔을 찍던 orders[1]입니다.
자주 틀리는 곳
틀린 생각: 타입은 모든 줄에 적어야 안전하다.
이름마다 다 적으면 꼼꼼해 보이지만, 덧붙인 타입은 검사기가 이미 아는 것을 다시 적을 뿐이고 적을 수 없는 자리도 있습니다.
const qtys = [2, 1];
let total: number = 0;
for (const q: number of qtys) {
total = total + q;
}
console.log(total);
main.ts(3,12): error TS2483: The left-hand side of a 'for...of' statement cannot use a type annotation.2번 줄의 : number는 첫 값 0에서 추론되는 타입을 다시 적은 것입니다. 3번 줄처럼 for...of가 꺼내는 이름에는 타입을 적을 수도 없습니다. Node도 이 줄을 문법 오류로 보고 돌리지 못합니다.
맞는 규칙: 검사기는 첫 값 · 반복이 꺼낸 값 · 함수가 돌려주는 값에서 타입을 추론합니다. 타입은 추론할 수 없는 곳 — 모양 · 매개변수 · 빈 객체와 빈 배열 — 에만 적습니다(검사 규칙 3).
틀린 생각: 검사 오류는 검색에서 찾은 any로 덮어도 된다.
1절의 TS7006을 검색하면 function label(o: any)처럼 any를 적는 답을 만납니다. 오류는 사라지지만, any는 o의 검사를 꺼서 2절에서 드러났던 qyt 오타가 다시 숨습니다. Node는 그대로 라테 undefined잔을 찍습니다.
맞는 규칙: 검사 오류는 검사기가 모르는 곳을 알려 주는 것이라, 덮지 않고 그곳의 타입을 적습니다 — 매개변수에는 Order, 빈 객체에는 인덱스 시그니처(any는 6단원 'any는 검사를 끈다').
틀린 생각: 검사 오류가 0이 되면 옮기기가 끝난 것이다.
검사기가 오류를 다 짚어 주니, 오류가 0이면 자바스크립트 때와 똑같이 돈다고 여기기 쉽습니다.
앞부분(3절과 같음)
interface Sold {
[item: string]: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];3절과 같은 앞부분에 이어, 세는 반복을 씁니다.
const sold: Sold = {};
for (const o of orders) {
const n = sold[o.item] ?? 0;
sold[o.item] = n + 1;
}
console.log(sold);
{ '커피': 1, '라테': 1, '쿠키': 1 }옮기면서 12번 줄을 n + o.qty가 아니라 n + 1로 썼습니다. 검사는 지나지만 잔 수가 아니라 주문 수를 세어, 커피가 2가 아니라 1입니다. 3절의 결과와 맞춰 봐야 드러납니다.
맞는 규칙: 검사기는 타입이 맞는지만 볼 뿐, 계산이 뜻대로인지는 모릅니다. 타입을 다 붙인 뒤에는 자바스크립트 때와 같은 값으로 돌려 출력이 같은지 봅니다(검사 규칙 1).
확인 문제
1. 2절 코드에서 12번 줄을 function label(o) {로 되돌렸습니다. 2번 하위 목표만 뺀 것입니다. 검사 결과는 어떻게 될까요?
// 1. 모양을 interface로
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
{ item: '쿠키', qty: 1 },
];
// 2. 매개변수에 타입
function label(o) {
return `${o.item} ${o.qyt}잔`;
}
console.log(label(orders[1]));
힌트
interface Order가 있어도, o가 Order라는 것을 검사기에게 알려 주는 곳이 어디인지 봅니다.
정답
12번 줄의 TS7006 하나만 납니다.
main.ts(12,16): error TS7006: Parameter 'o' implicitly has an 'any' type.interface Order를 적어 두어도 o에 붙이지 않으면 o는 any입니다. 그래서 13번 줄의 qyt 오타는 다시 숨습니다.
흔한 오답은 'TS2339도 함께 난다'입니다. interface만 있으면 검사기가 o를 Order로 알아본다고 본 것입니다. 검사기는 이름이 아니라 적힌 타입으로 읽고, 매개변수는 추론하지 않습니다.
2. 빈칸을 채워 코드를 완성하세요.
(가) 다 채운 코드가 검사 오류 0이고 라테 1을 찍도록 한 줄의 빈칸을 채우세요.
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
function show(o: ______) {
console.log(o.item, o.qty);
}
show(orders[1]);
힌트
2번 하위 목표 '매개변수에 타입'입니다.
정답
Order입니다.
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '커피', qty: 2 },
{ item: '라테', qty: 1 },
];
function show(o: Order) {
console.log(o.item, o.qty);
}
show(orders[1]);라테 1흔한 오답은 빈칸을 비워 show(o)로 두는 것입니다. 그러면 1절처럼 TS7006이 납니다(매개변수도 추론된다는 생각).
(나) 1번 하위 목표를 통째로 채우세요. 다 채운 코드는 검사 오류 0이고 쿠키 3을 찍어야 합니다.
// 1. 모양을 interface로
const orders = [
{ item: '쿠키', qty: 3 },
{ item: '녹차', qty: 2 },
];
function show(o: Order) {
console.log(o.item, o.qty);
}
show(orders[0]);
힌트
orders의 객체 하나를 보고 속성 줄마다 이름과 타입을 적습니다. 이름은 10번 줄이 쓰는 Order입니다.
정답
// 1. 모양을 interface로
interface Order {
item: string;
qty: number;
}
const orders = [
{ item: '쿠키', qty: 3 },
{ item: '녹차', qty: 2 },
];
function show(o: Order) {
console.log(o.item, o.qty);
}
show(orders[0]);쿠키 3다르게 써도 되는 곳: 줄의 차례는 상관없습니다. 줄 끝 ;은 ,로 써도 지나갑니다.
흔한 오답은 item: '쿠키';처럼 값을 적는 것입니다(타입과 값을 섞는 생각). 그러면 Order가 '쿠키'만 받는 타입이 되는데, orders의 item은 string으로 추론되어 13번 줄에서 막힙니다.
3. 장보기에 쓴 돈을 이름별로 모으는 자바스크립트 코드를 옮겼더니 7번 줄에서 검사에 막힙니다. 검사 오류를 알맞게 없애는 고치기는 어느 것일까요?
const cart = [
{ name: '우유', won: 2500 },
{ name: '빵', won: 3000 },
];
const spent = {};
for (const c of cart) {
spent[c.name] = c.won;
}
console.log(spent);
main.ts(7,3): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. No index signature with a parameter of type 'string' was found on type '{}'.- ①
interface Spent { [name: string]: number; }를 더하고 5번 줄을const spent: Spent = {};로 바꾼다. - ② 5번 줄을
const spent = { 우유: 0, 빵: 0 };으로 바꿔 칸을 미리 만든다. - ③ 6번 줄을
for (const c: string of cart) {로 바꾼다.
힌트
3번 하위 목표 '빈 객체와 빈 배열에 타입'입니다. 7번 줄에서 대괄호 안에 들어가는 c.name의 타입도 봅니다.
정답
①입니다.
interface Spent {
[name: string]: number;
}
const cart = [
{ name: '우유', won: 2500 },
{ name: '빵', won: 3000 },
];
const spent: Spent = {};
for (const c of cart) {
spent[c.name] = c.won;
}
console.log(spent);{ '우유': 2500, '빵': 3000 }②는 칸을 미리 만들어도 대괄호 안의 c.name이 string이라 같은 TS7053이 납니다. 앞 레슨 3절의 메뉴 표와 같은 까닭입니다. ③은 for...of가 꺼내는 이름에 타입을 적어 TS2483으로 막히고, 적을 수 있다 해도 c는 문자열이 아닙니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절 코드를 main.ts로 저장해 npx tsc로 검사합니다. 13번 줄의 qyt를 qty로 고쳐 다시 검사한 뒤 node main.ts로 돌려 라테 1잔이 나오는지 봅니다.
그다음 자바스크립트로 쓴 내 코드 하나를 골라, 하위 목표 넷을 따라 타입을 붙여 봅니다. 검사 오류가 0이 되면 자바스크립트 때와 같은 출력인지 맞춰 봅니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.