타입 자리에 적는 typeof 값이름은 이미 만들어 둔 자바스크립트 값에서 검사기가 추론한 타입 모양을 그대로 뽑아내는 문법입니다. 여기에 배열이나 객체 뒤에 붙이는 as const를 함께 쓰면, 값들이 string이나 number로 넓어지지 않고 리터럴 그대로 남아 (typeof 배열)[number] 한 줄로 리터럴 유니언 타입을 뽑아낼 수 있습니다.
코드에 이미 기본 설정 객체 latte나 크기 목록 배열 ['tall', 'grande']가 있는데, 똑같은 모양의 interface나 'tall' | 'grande' 타입을 손으로 한 번 더 적으면 값이 바뀔 때마다 두 군데를 고쳐야 합니다. 이 레슨에서는 값에서 타입을 뽑는 법을 살펴봅니다.
1타입 자리의 typeof — 값에서 모양 뽑기
JS 다시 보기: 실행 코드(값 자리)에서 typeof latte를 부르면 'object' 같은 글자 하나를 돌려줍니다(수·글자·불리언과 typeof).
반면 type Item = typeof latte;처럼 타입 자리에 적은 typeof는 실행되기 전에 검사기가 읽고 지우는 문법입니다. 검사기가 latte의 초기값에서 추론한 모양(name: string과 price: number)을 그대로 뽑아 새 타입으로 만듭니다(검사 규칙 3 · 4).
const latte = {
name: '라테',
price: 4500,
};
type Item = typeof latte;
const tea: Item = {
name: '녹차',
price: 4000,
};
console.log(
typeof latte,
tea.name,
);
object 녹차5번 줄의 typeof latte는 타입 자리라 name: string과 price: number 두 줄 모양이 되고, 12번 줄의 typeof latte는 console.log 인자(값 자리)라 실행할 때 'object'를 찍습니다.
| 적은 자리 | 예시 | 하는 일과 결과 |
|---|---|---|
| 값 자리 (실행 코드) | typeof latte | 실행 때 값의 종류를 글자('object')로 돌려줌 |
타입 자리 (: · type = 뒤) | type Item = typeof latte | 검사 때 값의 추론 모양을 뽑고 실행 전에 지워짐 |
2as const와 (typeof 배열)[number]로 리터럴 유니언 뽑기
적지 않아도 붙는 타입 — 추론과 리터럴 유니언으로 값 고르기에서 봤듯이, 배열이나 객체 속성은 나중에 바뀔 수 있다고 보고 ['tall', 'grande']를 string[]로 넓혀 추론합니다(검사 규칙 3).
값 뒤에 as const를 붙이면 검사기에게 '이 안의 값들은 바뀌지 않으니 넓히지 말고 적힌 리터럴 그대로 읽기 전용(readonly)으로 추론하라'고 알려 줍니다.
const sizes = [
'tall',
'grande',
] as const;
type Size =
(typeof sizes)[number];
const mySize: Size = 'short';
main.ts(7,7): error TS2322: Type '"short"' is not assignable to type '"grande" | "tall"'.판 차이 메모: 타입스크립트 7.0은 유니언을 알파벳순으로 정렬해 "grande" | "tall"로 찍고, 플레이그라운드(6.0)는 적은 차례대로 "tall" | "grande"로 찍습니다. 두 꼴은 같은 타입입니다.
1~4번 줄에서 as const를 붙였기 때문에 typeof sizes는 readonly ["tall", "grande"]가 됩니다. 앞 레슨 keyof와 속성 타입 꺼내기에서 객체 타입 뒤에 ['속성이름']을 붙여 속성 타입을 꺼냈듯이, 배열(튜플) 타입 뒤에 [number]를 붙이면 '숫자 인덱스로 꺼낼 수 있는 모든 원소의 타입'을 모아 "grande" | "tall" 리터럴 유니언을 만듭니다. 그래서 목록에 없는 'short'를 넣은 7번 줄이 TS2322로 막힙니다.
만약 4번 줄에서 as const를 빼면 7번 줄은 어떻게 될까요? 답을 정한 뒤 펼쳐 보세요.
const sizes = [
'tall',
'grande',
];
type Size =
(typeof sizes)[number];
const mySize: Size = 'short';
console.log(mySize);
검사 결과와 실행 결과 보기
short예상과 다르면 그곳이 배울 곳입니다.
as const를 빼면 sizes는 일반 배열 string[]로 넓혀집니다. 따라서 (typeof sizes)[number]도 넓은 string이 되어, 목록에 없는 'short'를 넣어도 아무 오류 없이 검사를 지나갑니다.
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
결제 수단 목록 배열 ways에 as const를 붙여 값으로도 쓰고(반복문 출력), 타입 Way로도 뽑아 함수 매개변수에 썼습니다.
const ways = [
'card',
'cash',
] as const;
type Way =
(typeof ways)[number];
function pay(w: Way) {
return `결제:${w}`;
}
for (const w of ways) {
console.log(pay(w));
}
console.log(pay('point'));
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
main.ts(14,17): error TS2345: Argument of type '"point"' is not assignable to parameter of type '"card" | "cash"'.결제:card
결제:cash
결제:point예상과 다르면 그곳이 배울 곳입니다.
Way는 "card" | "cash"이므로 11~13번 줄 반복문에서 꺼낸 w는 그대로 pay(w)에 들어가지만, 14번 줄의 'point'는 목록 밖이라 TS2345로 막힙니다.
Node는 as const와 타입을 모두 지우고 실행하므로(검사 규칙 1) 세 줄이 모두 찍힙니다.
자주 틀리는 곳
틀린 생각: 타입 자리에 이미 타입 이름을 놓고 typeof Menu처럼 적는다.
typeof가 타입을 다루는 키워드라고 생각하면 인터페이스나 타입 별칭 앞에도 typeof를 붙이기 쉽습니다(타입과 값을 섞는 생각).
interface Menu {
name: string;
}
type Copy = typeof Menu;
main.ts(4,20): error TS2693: 'Menu' only refers to a type, but is being used as a value here.맞는 규칙: 타입 자리의 typeof 뒤에는 반드시 값 이름(변수·상수·함수 이름)이 와야 합니다(1절). 이미 타입인 Menu는 typeof 없이 그대로 씁니다.
틀린 생각: as const도 as가 들어가므로 as는 검사기에게 하는 말의 타입 단언처럼 검사를 덮는 위험한 문법이다.
6단원에서 as Order 같은 단언이 실제 값과 다른 타입을 억지로 믿게 해 위험하다고 배웠기 때문에 as const도 비슷해 보입니다(단언이 값을 바꾼다는 생각).
맞는 규칙: as const는 없는 속성이 있다고 검사기를 속이지 않습니다. 오히려 코드에 직접 적은 리터럴 값을 string으로 넓히지 말고 더 좁고 정확하게(리터럴 + readonly) 추론하라고 알려 주는 안전한 표기입니다(2절 · 검사 규칙 3).
틀린 생각: const sizes에 ['tall', 'grande']를 넣었으니 as const 없이도 원소들이 리터럴로 추론된다.
변수 선언이 const이니 배열 안의 칸들도 바뀌지 않는 상수가 될 것 같습니다(넓어지지 않는다는 생각).
맞는 규칙: const는 변수 sizes에 다른 배열을 다시 대입하지 못하게 막을 뿐, 배열 안에 .push()로 새 글자를 넣는 것까지 막지 않습니다. 배열 원소나 객체 속성까지 리터럴 그대로 고정하려면 값 뒤에 as const를 붙여야 합니다(2절).
확인 문제
1. 아래 코드에서 6번 줄 Role의 타입은 무엇일까요? 7번 줄은 검사를 지나갈까요?
const roles = [
'admin',
'member',
];
type Role =
(typeof roles)[number];
const r: Role = 'guest';
힌트
1~4번 줄의 배열 뒤에 as const가 붙어 있는지 먼저 확인합니다(2절 맞혀 보기).
정답
Role의 타입은 string이고, 7번 줄은 오류 없이 검사를 지나갑니다.
roles 뒤에 as const가 없어서 roles가 string[]로 넓혀졌고, (typeof roles)[number]도 string이 되었기 때문입니다. 'admin' | 'member'만 허용하려면 4번 줄을 ] as const;로 고쳐야 합니다.
흔한 오답은 '7번 줄에서 TS2322로 막힌다'입니다. const roles만 보고 as const가 있는 것처럼 읽은 것입니다.
2. 기본 설정 객체 defaultConfig에서 타입을 뽑아 Config 별칭을 만들려고 합니다. 5번 줄 빈칸에 알맞은 두 단어를 채우세요.
const defaultConfig = {
dark: false,
fontSize: 16,
};
type Config = _____________;
const cfg: Config = {
dark: true,
fontSize: 20,
};
console.log(cfg.fontSize);
힌트
값 이름 defaultConfig 앞에 붙여 모양을 뽑아내는 키워드입니다(1절).
정답
빈칸은 typeof defaultConfig입니다.
const defaultConfig = {
dark: false,
fontSize: 16,
};
type Config =
typeof defaultConfig;
const cfg: Config = {
dark: true,
fontSize: 20,
};
console.log(cfg.fontSize);20다르게 써도 되는 곳: dark: boolean과 fontSize: number 두 줄을 직접 적어도 검사는 지나지만, defaultConfig에 줄이 추가될 때 자동으로 따라 바뀌지 않습니다.
흔한 오답은 typeof 없이 defaultConfig만 적는 것입니다. 값 이름을 타입 자리에 그대로 적으면 TS2749 오류가 납니다.
3. 요일 목록 배열에서 'mon' | 'wed' | 'fri' 리터럴 유니언 타입 Day를 뽑아 'sun'을 막으려고 합니다. 빈칸 ①과 ②에 들어갈 말을 바르게 짝지은 것을 고르세요.
const days = [
'mon',
'wed',
'fri',
] ①;
type Day = (typeof days)[②];
- ①
as const, ②number - ①
as string, ②0 - ①
readonly, ②string
힌트
배열이 넓어지지 않게 뒤에 붙이는 두 단어와, 배열 원소 전체의 타입을 꺼낼 때 대괄호 안에 적는 인덱스 타입을 떠올려 보세요(2절).
정답
첫 번째 보기(① as const, ② number)입니다. as const로 readonly ["mon", "wed", "fri"]를 만든 뒤 [number]로 모든 숫자 인덱스의 원소 타입을 모으면 "fri" | "mon" | "wed" 리터럴 유니언이 됩니다.
- 두 번째 보기에서
[0]을 적으면 첫째 원소'mon'하나만 꺼내집니다. - 세 번째 보기에서
readonly는 타입 자리에만 쓸 수 있고 값 뒤에는as const를 씁니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절 첫 코드를 main.ts에 저장해 npx tsc로 TS2322가 나는 것을 확인한 뒤, 4번 줄의 as const를 지우고 다시 npx tsc를 돌려 오류가 사라지는지 비교해 보세요. 다음 레슨은 있는 타입에서 새 타입 — Pick·Omit·Partial입니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.