🔥 typeof / keyof: 타입 레벨 첫 진입
강의 목차

같은 단어가 낯선 곳에 놓였을 때
IDE 자동완성에서 type Config = typeof config 를 처음 봤을 때 잠깐 멈췄다. typeof 는 if (typeof input === "string") 에서 값을 좁힐 때 쓰는 단어라고만 알고 있었다. 그런데 type 오른쪽에서도 같은 단어를 쓴다. 이 typeof 는 실행 중 값의 종류를 묻지 않는다. 타입 선언에 쓴다.
값에 타입을 붙이는 일과 타입 자체를 다루는 일은 다르다. 앞에서는 변수, 함수 인자, 반환값처럼 실행되는 코드 옆에 타입을 붙였다. 이제는 타입을 입력으로 받아 다른 타입을 만든다. typeof 와 keyof 는 그 첫 단계에 쓰기 좋은 두 연산자다.
값 위치의 typeof 와 타입 위치의 typeof
typeof 는 위치에 따라 다른 일을 한다. 값 위치의 typeof 는 JavaScript 런타임 연산자다. 타입 위치의 typeof 는 TypeScript 의 typeof 타입 연산자다.
값 위치에서는 실제 값을 받아 "string" 같은 문자열을 돌려준다. 이 패턴은 좁히기 편에서 이미 다뤘다. 타입 위치에서는 값 식별자를 받아 그 식별자의 정적 타입을 돌려준다. 같은 글자를 쓰지만 입력과 출력이 다르다.
// file: typeof-value-vs-type-position.ts
export {};
const input: string | number = Math.random() > 0.5 ? "ready" : 42;
if (typeof input === "string") {
const upper = input.toUpperCase();
console.log("value typeof:", upper);
} else {
console.log("value typeof:", input.toFixed(0));
}
type InputType = typeof input;
// ^? string | number
const sample: InputType = "done";
console.log("type typeof:", sample);// file: typeof-value-vs-type-position.ts
export {};
const input: string | number = Math.random() > 0.5 ? "ready" : 42;
if (typeof input === "string") {
const upper = input.toUpperCase();
console.log("value typeof:", upper);
} else {
console.log("value typeof:", input.toFixed(0));
}
type InputType = typeof input;
// ^? string | number
const sample: InputType = "done";
console.log("type typeof:", sample);첫 번째 typeof 는 if 조건 안에 있다. 이 코드는 실행 중 값을 검사한다. 두 번째 typeof 는 type InputType = 오른쪽에 있다. 이 코드는 실행되지 않고 타입 검사 때만 의미를 갖는다. 그래서 InputType 은 input 값의 현재 결과가 아니라 string | number 라는 정적 타입이 된다.

typeof 는 값에서 타입을 꺼낸다

타입 위치의 typeof 는 이미 존재하는 값 선언을 타입 선언의 재료로 쓴다. 객체를 먼저 만들고 그 객체와 같은 구조의 타입을 얻을 때 쓰기 좋다.
as const 를 붙이면 TypeScript 가 키와 값을 더 좁게 읽는다. 이 동작 자체는 단언 편에서 다뤘다. 지금은 결과만 쓴다. typeof config 는 config 값의 추론 결과를 그대로 타입으로 옮긴다.
// file: typeof-from-value.ts
export {};
const looseConfig = {
mode: "dark",
retry: 3,
};
type LooseConfig = typeof looseConfig;
// ^? { mode: string; retry: number }
const config = {
mode: "dark",
retry: 3,
features: {
search: true,
},
} as const;
type Config = typeof config;
// ^? { readonly mode: "dark"; readonly retry: 3; readonly features: { readonly search: true } }
const currentMode: Config["mode"] = "dark";
const retryCount: LooseConfig["retry"] = 3;
console.log("mode:", currentMode);
console.log("retry:", retryCount);// file: typeof-from-value.ts
export {};
const looseConfig = {
mode: "dark",
retry: 3,
};
type LooseConfig = typeof looseConfig;
// ^? { mode: string; retry: number }
const config = {
mode: "dark",
retry: 3,
features: {
search: true,
},
} as const;
type Config = typeof config;
// ^? { readonly mode: "dark"; readonly retry: 3; readonly features: { readonly search: true } }
const currentMode: Config["mode"] = "dark";
const retryCount: LooseConfig["retry"] = 3;
console.log("mode:", currentMode);
console.log("retry:", retryCount);LooseConfig 에서 mode 는 string 이다. config 에 as const 를 붙이면 Config["mode"] 는 "dark" 가 된다. typeof 는 값을 새로 만들지 않는다. 이미 추론된 값의 타입을 다른 타입 선언에서 다시 쓰게 한다.
keyof 는 타입에서 키 유니온을 꺼낸다
keyof 는 타입의 속성 이름을 유니온으로 만든다. 입력은 타입이고 출력도 타입이다. 그래서 keyof 는 타입 위치에서만 의미가 있다.
객체 타입에 id, name, age 속성이 있으면 keyof 결과는 "id" | "name" | "age" 다. 이 유니온은 함수 인자나 변수 타입에 바로 쓸 수 있다. 잘못된 키를 문자열로 넘기면 타입 검사가 막는다.
// file: keyof-of-type.ts
export {};
type Person = {
id: string;
name: string;
age: number;
};
type PersonKey = keyof Person;
// ^? "id" | "name" | "age"
const key: PersonKey = "id";
const anotherKey: PersonKey = "name";
// const wrongKey: PersonKey = "email";
// Type '"email"' is not assignable to type 'keyof Person'.
console.log("person keys:", key, anotherKey);// file: keyof-of-type.ts
export {};
type Person = {
id: string;
name: string;
age: number;
};
type PersonKey = keyof Person;
// ^? "id" | "name" | "age"
const key: PersonKey = "id";
const anotherKey: PersonKey = "name";
// const wrongKey: PersonKey = "email";
// Type '"email"' is not assignable to type 'keyof Person'.
console.log("person keys:", key, anotherKey);PersonKey 는 아무 문자열이 아니다. Person 타입에 있는 세 개 키만 받는다. 문자열을 직접 적으면 오타를 그냥 지나치기 쉽다. keyof 를 쓰면 허용할 키 범위를 타입으로 제한할 수 있다.

typeof 와 keyof 를 합치면 값에서 키까지 간다

typeof 와 keyof 는 이어서 쓸 수 있다. 먼저 값에서 타입을 꺼내고, 그 타입에서 키 유니온을 꺼낸다.
이 흐름은 객체를 기준으로 API 이름, 색 이름, 설정 키를 제한할 때 자주 쓴다. 객체가 실제 데이터의 기준이 되고, 타입은 그 객체에서 나온다. 키 목록을 따로 적지 않으므로 객체와 타입이 따로 변하지 않는다.
// file: typeof-then-keyof.ts
export {};
const palette = {
primary: "#3178c6",
danger: "#dc2626",
} as const;
type Palette = typeof palette;
// ^? { readonly primary: "#3178c6"; readonly danger: "#dc2626" }
type PaletteKey = keyof Palette;
// ^? "primary" | "danger"
function color(name: PaletteKey): string {
return palette[name];
}
const primaryColor = color("primary");
// const ghostColor = color("ghost");
// Argument of type '"ghost"' is not assignable to parameter of type '"primary" | "danger"'.
console.log("primary:", primaryColor);// file: typeof-then-keyof.ts
export {};
const palette = {
primary: "#3178c6",
danger: "#dc2626",
} as const;
type Palette = typeof palette;
// ^? { readonly primary: "#3178c6"; readonly danger: "#dc2626" }
type PaletteKey = keyof Palette;
// ^? "primary" | "danger"
function color(name: PaletteKey): string {
return palette[name];
}
const primaryColor = color("primary");
// const ghostColor = color("ghost");
// Argument of type '"ghost"' is not assignable to parameter of type '"primary" | "danger"'.
console.log("primary:", primaryColor);palette 객체에는 primary 와 danger 만 있다. PaletteKey 도 두 키만 갖는다. color 함수는 그 키만 받기 때문에 "ghost" 같은 값은 타입 검사에서 걸린다. 실행 전에 잘못된 이름을 막는 구조다.
타입 연산자도 함수처럼 이어서 쓴다

타입 위치의 연산자는 값을 실행하지 않고 타입을 계산한다. typeof palette 는 값에서 타입을 얻고, keyof Palette 는 타입에서 키 유니온을 얻는다. 입력이 있고 출력이 있다. 이 점에서 함수 호출과 비슷하게 읽으면 된다.
앞으로 더 복잡한 타입 문법이 나와도 출발점은 같다. 타입 하나를 넣어 다른 타입을 얻는다. 이번 글을 쓰고 나니 타입 문법을 특별한 장치로 보기보다, 입력과 출력이 있는 작은 계산으로 보게 됐다.












