선택 속성(optional property)은 memo?: string처럼 이름 뒤에 ?를 붙여 '없어도 되는 줄'로 적은 속성이고, readonly는 '이 속성에는 다시 대입하지 않는다'는 표시입니다. 검사기는 선택 속성을 꺼낸 값이 undefined일 수 있다고 보고, readonly 속성에 대입하는 줄을 짚습니다. 둘 다 검사기가 읽는 약속이라 실행할 때는 남지 않습니다.
주문에는 메모가 있기도 하고 없기도 합니다. 앞 레슨의 모양은 줄이 모두 있어야 해서, 메모 없는 주문을 Order 자리에 넣으면 TS2741로 막힙니다. 도장 카드 번호처럼 한 번 정하면 바꾸지 않을 속성도 있습니다. 이런 속성을 실수로 바꾸는 줄을 검사기가 먼저 짚어 주면, 어디서 바뀌었는지 찾지 않아도 됩니다.
1선택 속성 — 없어도 되는 줄
속성 이름 뒤에 ?를 붙이면 그 줄은 없어도 됩니다.
interface Order {
item: string;
qty: number;
memo?: string;
}
const a: Order = {
item: '커피',
qty: 2,
memo: '얼음 적게',
};
const b: Order = {
item: '라테',
qty: 1,
};
console.log(a.memo, b.memo);
얼음 적게 undefined4번 줄 memo?: string은 'memo 줄은 없어도 되고, 있으면 string'이라는 뜻입니다. memo가 있는 a도, 없는 b도 Order 자리에 들어 검사를 지나갑니다.
memo가 없는 b에서 memo를 꺼내면 자바스크립트처럼 undefined입니다. 그래서 검사기는 선택 속성을 꺼낸 값의 타입을 string | undefined, 곧 'string이거나 undefined'로 봅니다. 앞 단원의 선택 매개변수와 같은 꼴입니다.
15번 줄에서 a.memo의 지금 타입도 string | undefined입니다. a에는 memo가 있지만, 검사기는 값이 아니라 a의 선언 타입 Order를 읽기 때문입니다.
꺼낸 memo를 바로 쓰면 어떻게 될까요? 아래 코드의 검사 결과와 실행 결과를 정한 뒤 펼쳐 보세요.
interface Order {
item: string;
qty: number;
memo?: string;
}
const o: Order = {
item: '라테',
qty: 1,
};
console.log(o.memo.length);
검사 결과와 실행 결과 보기
main.ts(11,13): error TS18048: 'o.memo' is possibly 'undefined'.main.ts:11console.log(o.memo.length); ^ TypeError: Cannot read properties of undefined (reading 'length') at main.ts:11:20예상과 다르면 그곳이 배울 곳입니다.
is possibly 'undefined'는 'undefined일 수 있다'는 뜻이고, 문장 전체는 'o.memo는 undefined일 수 있다'입니다. o.memo의 지금 타입에 undefined가 들어 있는데, undefined에는 없는 length를 꺼내려 해서 짚었습니다.
Node는 검사하지 않고 돌다가 11번 줄에서 TypeError로 멈춥니다. 검사기가 짚은 바로 그 줄입니다.
없을 때 쓸 값을 정해 두면 검사를 지나갑니다.
JS 다시 보기: ??는 왼쪽이 null·undefined일 때만 오른쪽을 씁니다(JS '참 같은 값과 ??').
JS 다시 보기: ?.는 왼쪽이 null·undefined이면 더 꺼내지 않고 undefined를 줍니다(JS '객체를 담은 배열').
interface Order {
item: string;
qty: number;
memo?: string;
}
const o: Order = {
item: '라테',
qty: 1,
};
console.log(o.memo ?? '없음');
console.log(o.memo?.length);
없음
undefined11번 줄은 memo가 없으면 '없음'을 씁니다. 12번 줄은 memo가 없으면 length를 꺼내지 않고 undefined를 줍니다. 둘 다 없는 경우를 함께 다뤄서 검사기가 짚지 않습니다.
if로 있는지 확인한 뒤 쓰는 길은 뒤의 'null·undefined 거르기' 레슨에서 다룹니다.
2readonly — 검사기만 막는 자물쇠
속성 이름 앞에 readonly를 붙이면 그 속성에 다시 대입하는 줄을 검사기가 짚습니다. 도장 카드의 번호 id에 붙였습니다. 검사 결과와 실행 결과를 정한 뒤 펼쳐 보세요.
interface Card {
readonly id: string;
stamps: number;
}
const card: Card = {
id: 'c1',
stamps: 0,
};
card.stamps = 1;
card.id = 'c2';
console.log(card.id);
검사 결과와 실행 결과 보기
main.ts(11,6): error TS2540: Cannot assign to 'id' because it is a read-only property.c2예상과 다르면 그곳이 배울 곳입니다.
Cannot assign to 'id'는 'id에 대입할 수 없다', 뒤의 read-only property는 'readonly 속성'이라는 뜻입니다. 곧 id는 readonly 속성이라 대입할 수 없다고 짚었습니다. readonly가 없는 stamps에 대입한 10번 줄은 지나갑니다.
그런데 Node는 c2를 찍습니다. readonly도 interface 안의 타입 글자라 Node가 지우고 돌리므로, 실행할 때 11번 줄을 막는 것이 없습니다(검사 규칙 1).
readonly는 '이 속성은 바꾸지 않는다'는 약속을 검사기가 지켜 보게 하는 표시입니다. 바꾸는 줄이 있으면 실행하기 전에 그 줄을 짚습니다.
card는 const인데 10번 줄이 지나가는 것도 봅니다.
JS 다시 보기: const는 이름을 다른 값에 다시 붙이는 것만 막습니다(JS '이름표 다시 붙이기와 const'). 그 객체의 속성은 바꿀 수 있습니다(JS '객체를 넘기고 복사하기').
const와 readonly는 지키는 것이 다릅니다.
const card | readonly id | |
|---|---|---|
| 막는 것 | card를 다른 값에 다시 붙이기 | id에 다시 대입하기 |
| 검사할 때 | TS2588로 짚음 | TS2540으로 짚음 |
| 실행할 때 | TypeError로 멈춤 | 막지 않음 |
예제
번호는 readonly, 메모는 선택
주문 번호 id는 바꾸지 않을 속성이라 readonly, 메모는 없어도 되는 속성이라 선택 속성으로 적었습니다.
interface Order {
readonly id: number;
item: string;
memo?: string;
}
const o: Order = {
id: 1,
item: '라테',
};
const memo = o.memo ?? '없음';
console.log(o.id, o.item, memo);
1 라테 없음memo가 없는 객체도 Order 자리에 들어 검사를 지나갑니다. 11번 줄의 ??가 없는 메모를 '없음'으로 바꿔, 12번 줄은 늘 글자를 찍습니다.
자주 틀리는 곳
틀린 생각: 선택 속성은 꺼내면 늘 값이 들어 있다.
memo가 있는 주문만 보며 코드를 쓰면, memo가 없는 주문을 잊기 쉽습니다. 검사기가 늘 짚어 주는 것도 아닙니다.
interface Order {
item: string;
qty: number;
memo?: string;
}
const o: Order = {
item: '라테',
qty: 1,
};
console.log(`메모: ${o.memo}`);
메모: undefined템플릿 문자열은 undefined도 글자로 이어 붙이므로 검사기가 짚을 것이 없습니다. 검사를 지났지만 영수증에는 메모: undefined가 찍힙니다.
맞는 규칙: 선택 속성의 지금 타입에는 undefined가 들어 있어, 꺼낸 값이 없을 수 있습니다. 검사기가 짚지 않는 자리에서도 o.memo ?? '없음'처럼 없을 때 쓸 값을 정해 둡니다(검사 규칙 2).
틀린 생각: readonly를 붙이면 실행할 때도 그 속성이 바뀌지 않는다.
readonly라는 낱말 때문에 값이 잠기는 것처럼 보입니다. 2절에서 보았듯 검사기는 11번 줄을 짚지만, Node는 대입을 그대로 하고 c2를 찍습니다.
맞는 규칙: readonly는 검사기만 읽는 표시라 Node가 돌 때는 지워지고 없습니다. 실행 중에도 바뀌면 안 되는 값은 자바스크립트의 Object.freeze가 맡습니다(검사 규칙 1).
틀린 생각: readonly 배열 속성은 배열 안의 값도 바꿀 수 없다.
readonly를 붙인 stamps라면 도장을 더하는 push도 막힐 것 같습니다.
interface Card {
readonly id: string;
readonly stamps: number[];
}
const card: Card = {
id: 'c1',
stamps: [],
};
card.stamps.push(1);
console.log(card.stamps);
card.stamps = [1, 2];
main.ts(12,6): error TS2540: Cannot assign to 'stamps' because it is a read-only property.[ 1 ]검사기가 짚은 것은 stamps에 새 배열을 대입한 12번 줄뿐입니다. 10번 줄의 push는 stamps에 대입하지 않고 이미 있는 배열 안을 바꿀 뿐이라 지나갑니다.
배열 안까지 막으려면 타입 쪽에 readonly number[]로 적습니다.
interface Card {
readonly id: string;
stamps: readonly number[];
}
const card: Card = {
id: 'c1',
stamps: [],
};
card.stamps.push(1);
main.ts(10,13): error TS2339: Property 'push' does not exist on type 'readonly number[]'.맞는 규칙: 속성 앞의 readonly는 그 속성에 다른 값을 대입하는 줄만 막고, 이미 있는 배열 안을 바꾸는 push는 막지 않습니다. 배열 안까지 막으려면 타입을 readonly number[]로 적습니다.
확인 문제
1. 1절 첫 코드의 15번 줄에서 a.memo의 지금 타입은 무엇일까요? 검사기가 찍는 글자 그대로 쓰세요.
interface Order {
item: string;
qty: number;
memo?: string;
}
const a: Order = {
item: '커피',
qty: 2,
memo: '얼음 적게',
};
const b: Order = {
item: '라테',
qty: 1,
};
console.log(a.memo, b.memo);
힌트
a의 값이 아니라, 검사기가 a를 읽을 때 쓰는 타입을 봅니다.
정답
string | undefined입니다.
검사기는 a의 값을 보지 않고 a의 선언 타입 Order를 읽습니다. Order의 memo는 선택 속성이라 꺼낸 값의 타입에 undefined가 들어 있습니다. 실행하면 '얼음 적게'가 찍히지만, 지금 타입은 b.memo와 같습니다.
흔한 오답은 string입니다. a에는 memo가 있으니 늘 있다고 본 것입니다(꺼낸 값은 늘 있다는 생각).
2. 빈칸 둘을 채워, 이 코드가 검사를 지나고 7 라테를 찍게 하세요. 그리고 이 코드 끝에 o.id = 8;을 더하면 검사에서 막혀야 합니다.
interface Order {
________ id: number;
item: string;
memo____ string;
}
const o: Order = {
id: 7,
item: '녹차',
};
o.item = '라테';
console.log(o.id, o.item);
힌트
memo가 없는 객체가 Order 자리에 들어가야 합니다. id는 다시 대입하는 줄만 막고, item은 바꿀 수 있어야 합니다.
정답
interface Order {
readonly id: number;
item: string;
memo?: string;
}
const o: Order = {
id: 7,
item: '녹차',
};
o.item = '라테';
console.log(o.id, o.item);7 라테끝에 o.id = 8;을 더하면 그 줄에서 TS2540이 납니다. item에는 readonly가 없어 11번 줄은 지나갑니다.
흔한 오답은 memo: string?처럼 ?를 타입 뒤에 쓰는 것입니다. 검사기는 TS17019로 짚고, Node도 문법 오류로 돌지 않습니다. ?는 속성 이름 바로 뒤, : 앞에 붙입니다. id: readonly number처럼 타입 앞에 readonly를 쓰는 것도 TS1354로 막힙니다. readonly는 속성 이름 앞에 붙입니다.
3. 장보기 목록의 한 줄을 다루는 코드입니다. 검사를 지나갈까요? 막힌다면 몇 번 줄일까요? Node로 돌리면 무엇이 찍힐까요?
interface Item {
readonly name: string;
count: number;
note?: string;
}
const egg: Item = {
name: '달걀',
count: 10,
};
egg.count = 12;
egg.name = '메추리알';
console.log(egg.name, egg.note);
힌트
readonly가 붙은 속성에 대입하는 줄을 찾습니다. 실행 결과는 Node가 무엇을 지우고 도는지 떠올리며 13번 줄까지 따라갑니다.
정답
검사는 12번 줄에서 막힙니다.
main.ts(12,5): error TS2540: Cannot assign to 'name' because it is a read-only property.Node는 메추리알 undefined를 찍습니다.
메추리알 undefinedreadonly는 검사기만 막습니다. Node는 12번 줄을 그대로 실행해 name이 '메추리알'이 되고, note 줄이 없는 egg에서 꺼낸 note는 undefined입니다.
흔한 오답은 달걀 undefined입니다. readonly가 실행할 때도 대입을 막는다고 본 것입니다(타입이 실행 때 남는다는 생각). 실행이 12번 줄에서 멈춘다는 답은 검사 오류가 있으면 Node도 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각).
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 첫 코드를 main.ts로 저장해 npx tsc로 검사하고 node main.ts로 돌립니다. 그다음 4번 줄 memo?: string에서 ?를 지우면 검사 결과가 어떻게 바뀔지 먼저 적고, 다시 검사해서 맞춰 봅니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.