const-tommy.dev
기록을 불러오는 중입니다
satisfies, 타입 검증, 타입 추론 보존, 리터럴 타입 좁히기TypeScript 4.9 이전에는 객체 상수를 선언할 때 두 가지 방법뿐이었고, 둘 다 한쪽을 포기해야 했다.
방법 A — 타입 어노테이션 (: Type)
const NODE_PORT_RULES: Record<NodeType, { hasInput: boolean; hasOutput: boolean }> = {
input: { hasInput: false, hasOutput: true },
model: { hasInput: true, hasOutput: true },
output: { hasInput: true, hasOutput: false },
};
// ✅ 얻는 것: NodeType 키 누락 시 컴파일 에러
// ❌ 잃는 것: 값의 타입이 boolean으로 "넓어짐"
NODE_PORT_RULES.input.hasInput; // 타입: boolean ← 실제론 항상 false인데!어노테이션은 "이 변수는 이 타입이다" 라고 선언하는 것이다. 그래서 실제 값이 더
구체적이더라도(리터럴 false), 변수의 타입은 선언한 대로(boolean) 확정된다.
이것을 타입 넓히기(widening) 라고 한다.
방법 B — as const
const NODE_PORT_RULES = {
input: { hasInput: false, hasOutput: true },
model: { hasInput: true, hasOutput: true },
} as const;
// ✅ 얻는 것: 리터럴 타입 유지 (hasInput: false)
// ❌ 잃는 것: NodeType을 전부 커버했는지 검사 안 함 → 'output' 빠져도 조용히 통과딜레마: "키 누락은 잡아주면서, 값의 리터럴 타입은 살리고 싶다."
이 요구를 해결하려고 나온 게 satisfies다.
핵심 차이는 "선언"이냐 "검사"냐
어노테이션 : T | satisfies T | |
|---|---|---|
| 역할 | 타입을 강제 지정 | 타입 만족 여부를 검사만 |
| 결과 타입 | T (넓어짐) | 추론된 실제 타입 (유지) |
| 키 누락 검사 | ✅ | ✅ |
| 초과 프로퍼티 검사 | ✅ | ✅ |
satisfies는 컴파일 타임에 "이 값이 T에 대입 가능한가?"만 확인하고 통과시킨다.
검사 후에는 손을 떼기 때문에, 값의 원래 추론 타입이 그대로 남는다.
as const satisfies T 조합이 정답인 이유
const RULES = { ... } as const satisfies Record<NodeType, PortRule>;
// ───────────── ──────────────────────────────────
// 1. 리터럴로 좁힘 2. 좁혀진 값이 Record를 만족하는지 검사순서가 중요하다. as const가 먼저 값을 리터럴로 고정하고, 그 결과를 satisfies가
검사한다. 검사만 하므로 리터럴이 살아남는다.
⚠️ 흔한 실수 — 어노테이션 + as const 혼용
// ❌ as const가 무력화된다
const RULES: Record<NodeType, PortRule> = { ... } as const;as const로 리터럴로 좁혀도, 좌변의 어노테이션이 변수 타입을 Record<...>로
다시 확정해버린다. 결국 as const는 아무 효과 없이 코드만 지저분해진다.
어노테이션과 as const는 함께 쓰지 않는다.
type NodeType = 'input' | 'model' | 'output';
interface PortRule {
hasInput: boolean;
hasOutput: boolean;
}
// ✅ 권장: as const satisfies
export const NODE_PORT_RULES = {
input: { hasInput: false, hasOutput: true
다른 흔한 활용 — 키 목록 추출
const THEME = {
primary: '#0f172a',
muted: '#64748b',
} as const satisfies Record<string, string>;
type ThemeKey = keyof typeof THEME; // 'primary' | 'muted'
// 어노테이션(: Record<string, string>)을 썼다면 keyof는 그냥 string이 됐을 것Q1. as const는 정확히 뭘 하는가?
A. 세 가지를 한다. ① 원시값을 리터럴 타입으로 좁힘 (false → false 타입),
② 모든 프로퍼티를 readonly로, ③ 배열을 튜플로. 즉 "이 값은 절대 안 변한다"고
선언해 TS가 최대한 구체적으로 추론하게 만든다.
Q2. 그럼 satisfies만 쓰면 안 되나? as const 없이.
A. 가능하지만 리터럴이 안 남는다. satisfies는 검사만 할 뿐 추론을 바꾸지 않는데,
객체 리터럴의 기본 추론은 이미 boolean으로 넓혀진 상태다. as const가 있어야
추론 단계에서 false로 좁혀지고, satisfies가 그걸 보존해준다. 역할이 다르다:
as const는 좁히고, satisfies는 검사하고 보존한다.
Q3. satisfies가 잡아주는 에러는 어노테이션과 완전히 같은가?
A. 거의 같다. 키 누락, 타입 불일치, 초과 프로퍼티 모두 잡는다. 차이는 결과 타입뿐.
그래서 "검사는 그대로, 추론만 살린다"가 정확한 요약이다.
Q4. 언제 어노테이션을 쓰는 게 맞나?
A. 값이 나중에 재할당되거나 변할 때, 또는 의도적으로 넓은 타입이 필요할 때.
예를 들어 함수 파라미터나 상태 변수는 어노테이션이 맞다. satisfies는 주로
불변 상수 테이블(설정, 매핑, 토큰)에 쓴다.
Q5. 런타임에 영향이 있나?
A. 전혀 없다. satisfies와 as const 모두 순수 컴파일 타임 기능이라 JS로
트랜스파일되면 사라진다. 번들 크기 영향 0.
: T)은 타입을 "덮어씌우고", satisfies T는 "검사만 한다". 그래서
satisfies는 값의 구체적인 추론 타입을 보존한다.as const satisfies T가 상수 테이블의 정석 패턴. as const로 리터럴을 고정하고,
satisfies로 스키마 준수를 검증한다. 키 누락 검사 + 리터럴 유지를 동시에 얻는다.as const를 함께 쓰면 as const가 무력화된다. 좌변 어노테이션이
타입을 다시 확정하기 때문. 둘 중 하나만 쓰거나, satisfies로 대체할 것.