- Published on
TS 5.x satisfies로 타입 오류를 줄이는 실전 패턴
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
TypeScript를 오래 쓰다 보면, “타입 안전하게 만들려고 했는데 오히려 타입이 뭉개져서(너무 넓어져서) IDE 자동완성도 줄고, 결국 as를 남발하게 되는” 순간이 자주 옵니다. 특히 설정 객체(config), 라우트/이벤트 테이블, 상수 맵 같은 객체 리터럴을 다룰 때가 그렇습니다.
TS 4.9부터(5.x에서도 동일하게) 제공되는 satisfies는 이 문제를 정면으로 해결합니다.
: Type주석(annotation)은 값을 Type으로 강제하면서 추론 정보를 잃기 쉽고as Type단언(assertion)은 검증 없이 덮어씌우기 때문에 런타임 버그를 숨길 수 있는데satisfies Type은 “이 값이 Type을 만족하는지”만 검사하고, 값 자체의 구체적인 추론 타입은 유지합니다.
이 글에서는 satisfies가 왜 타입 오류를 줄이는지, 그리고 실무에서 어떤 패턴으로 쓰면 좋은지 코드로 정리합니다.
satisfies가 해결하는 핵심 문제
1) 타입 주석(:)이 추론을 망가뜨리는 경우
다음은 흔한 패턴입니다. Record<string, ...>로 설정 맵을 선언해두면 깔끔해 보이지만, 키가 전부 string으로 넓어져 버립니다.
type Handler = (payload: unknown) => void;
const handlers: Record<string, Handler> = {
userCreated: (payload) => {
// payload는 unknown
},
orderPaid: (payload) => {
// payload는 unknown
},
};
// handlers.userCreated는 존재하지만,
// 키 자동완성/리터럴 유니온(userCreated | orderPaid)을 잃기 쉽다.
Record<string, ...>로 주석을 달면, 객체 리터럴의 "구체 키" 정보가 string으로 일반화되기 쉽습니다. 결과적으로 다른 코드에서 keyof typeof handlers를 써도 기대한 리터럴 유니온이 안 나오거나, 키 오타를 잡는 능력이 약해집니다.
2) 단언(as)이 오류를 숨기는 경우
type Env = "dev" | "prod";
const config = {
env: "production", // 오타
} as { env: Env };
// 컴파일은 통과하지만 런타임에서 분기 로직이 깨질 수 있다.
as는 “내가 맞다고 할 테니 믿어”에 가깝습니다. 검증이 없으니 타입 오류를 줄이는 게 아니라 숨깁니다.
3) satisfies는 검증 + 추론 유지
type Env = "dev" | "prod";
type AppConfig = {
env: Env;
port: number;
};
const config = {
env: "prod",
port: 3000,
// debug: true, // 필요하면 추가 가능(아래 섹션에서 설명)
} satisfies AppConfig;
// config.env는 "prod" (리터럴)로 유지
// config.port는 3000 (리터럴)로 유지
여기서 중요한 포인트는:
config는AppConfig를 만족하는지 검사된다.- 하지만
config의 타입은AppConfig로 “캐스팅”되지 않고, 리터럴 기반의 구체 타입이 유지된다.
즉, 타입 오류를 줄이면서도 DX(자동완성/리팩터링/정적 분석)를 더 좋게 만듭니다.
패턴 1: 설정 객체(config)에서 satisfies로 오타/누락 잡기
환경별 설정을 예로 들어봅시다.
type Stage = "local" | "dev" | "prod";
type StageConfig = {
apiBaseUrl: string;
timeoutMs: number;
};
const stageConfig = {
local: { apiBaseUrl: "http://localhost:3000", timeoutMs: 3000 },
dev: { apiBaseUrl: "https://dev.example.com", timeoutMs: 5000 },
prod: { apiBaseUrl: "https://example.com", timeoutMs: 8000 },
// prdo: { ... } // 오타가 있으면 아래 satisfies에서 잡힘
} satisfies Record<Stage, StageConfig>;
function getConfig(stage: Stage) {
return stageConfig[stage];
}
장점:
Record<Stage, StageConfig>를 만족해야 하므로local/dev/prod누락 시 컴파일 에러prdo같은 오타 키는 “필요한 키가 없다”는 형태로 에러를 유도- 동시에
stageConfig.prod.timeoutMs같은 접근에서 값이 리터럴로 유지되어, 조건 분기 최적화/자동완성에 유리
실무에서 이런 설정은 배포/인프라 장애와도 연결됩니다. 예를 들어 EKS에서 네트워크/인증 설정이 꼬여 장애가 날 때, 원인이 단순한 설정 키 오타/누락인 경우도 있습니다. 인프라 트러블슈팅 글을 참고해보면 설정 검증의 중요성이 더 체감됩니다: EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS
패턴 2: 라우트/이벤트 맵에서 “키는 고정, 값은 유연”하게
라우팅 테이블이나 이벤트 핸들러 맵을 만들 때, 키는 제한하고 값은 각자 다른 형태를 갖습니다. 이때 satisfies가 특히 강력합니다.
type Routes = "/" | "/login" | "/settings";
type RouteMeta = {
authRequired: boolean;
title: string;
};
const routeMeta = {
"/": { authRequired: false, title: "Home" },
"/login": { authRequired: false, title: "Login" },
"/settings": { authRequired: true, title: "Settings" },
// "/setings": { ... } // 오타는 satisfies에서 잡힘
} satisfies Record<Routes, RouteMeta>;
// keyof typeof routeMeta는 정확히 Routes로 좁혀짐
이 방식은 다음을 동시에 만족합니다.
- 키의 제한(오타 방지, 누락 방지)
- 객체 리터럴의 구체 타입 유지(필요하면 더 정밀한 추론)
패턴 3: API 스키마/계약을 객체로 관리할 때
백엔드/외부 API 호출을 래핑할 때, 엔드포인트별로 메서드와 응답 타입을 매핑하는 패턴이 자주 나옵니다.
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type EndpointSpec = {
method: HttpMethod;
path: string;
timeoutMs?: number;
};
type EndpointName = "getUser" | "createPost";
const endpoints = {
getUser: { method: "GET", path: "/v1/user", timeoutMs: 3000 },
createPost: { method: "POST", path: "/v1/posts" },
// createpost: { ... } // 오타
// getUser: { method: "FETCH", path: "/v1/user" } // method 오타
} satisfies Record<EndpointName, EndpointSpec>;
function buildUrl(name: EndpointName) {
return endpoints[name].path;
}
여기서 endpoints는 단지 타입을 “맞춘다” 수준이 아니라, 계약(Contract)을 코드로 고정합니다. 이 스타일은 OpenAI 같은 외부 API를 다룰 때도 유용합니다. 예를 들어 요청/응답 구조가 조금만 어긋나도 400이 터지는 환경에서는, 객체 기반 계약 검증이 큰 도움이 됩니다: OpenAI Responses API 400 invalid_tool_output 해결법
패턴 4: as const + satisfies 조합으로 리터럴을 끝까지 살리기
as const는 값을 최대한 리터럴로 고정하지만, 그 자체로는 “특정 타입을 만족하는지”를 강제하지 않습니다. 둘을 조합하면 다음이 가능합니다.
type LogLevel = "debug" | "info" | "warn" | "error";
type LoggerConfig = {
level: LogLevel;
json: boolean;
};
const loggerConfig = {
level: "info",
json: true,
} as const satisfies LoggerConfig;
// level은 "info"로 유지되면서, LogLevel에 속하는지도 검증됨
주의할 점:
as const는 객체의 속성을readonly로 만들고 리터럴로 고정합니다.- 여기에
satisfies를 붙이면 “너무 빡빡하게 고정된 값”이 타입을 만족하는지까지 체크합니다.
설정값을 불변으로 다루고 싶은 코드베이스에서 특히 유용합니다.
패턴 5: satisfies로 “과한 일반화”를 피하고, 필요한 곳에서만 일반화
const x: SomeType = ...는 x를 SomeType으로 만들기 때문에, 이후 로직에서 더 구체적인 정보가 필요해도 사라져 버립니다. satisfies는 “검증만 하고 타입은 유지”하기 때문에, 필요할 때만 일반화할 수 있습니다.
type FeatureFlag = {
enabled: boolean;
rollout: number; // 0~100
};
const flags = {
newCheckout: { enabled: true, rollout: 10 },
newSearch: { enabled: false, rollout: 0 },
} satisfies Record<string, FeatureFlag>;
// flags는 구체 키(newCheckout, newSearch)를 유지
// 하지만 값은 FeatureFlag 형태를 강제
function isEnabled(name: keyof typeof flags) {
return flags[name].enabled;
}
이 패턴은 점진적으로 기능 플래그가 늘어나는 서비스에서, 키 오타/값 누락을 줄이는 데 효과적입니다.
satisfies를 쓸 때 자주 하는 오해/주의점
1) “초과 속성(excess property)” 검사는 어떻게 되나?
객체 리터럴을 satisfies로 검사할 때도 기본적으로 타입 체크가 일어나며, 상황에 따라 초과 속성 관련 경고를 기대할 수 있습니다. 다만 satisfies는 “변수의 타입을 그 타입으로 바꾸지 않는다”는 점이 핵심이므로, 초과 속성을 엄격히 금지하는 설계를 원한다면 다음 중 하나를 고려하세요.
- 타입 자체를
Exact유틸로 강제(커스텀 타입) - 혹은 객체를 한 번 더 함수 인자로 넘겨 “정확성”을 검사하는 패턴
2) satisfies는 런타임 검증이 아니다
zod, io-ts처럼 런타임 스키마 검증을 해주지 않습니다. 외부 입력(JSON, API response)을 신뢰할 수 없다면 런타임 검증이 별도로 필요합니다.
3) 인덱스 시그니처/Record를 남발하면 키가 다시 넓어진다
Record<string, T>는 키를 string으로 넓히는 설계입니다. 키 유니온을 유지하려면 가능한 한 Record<"a"|"b", T>처럼 구체 키 유니온을 쓰거나, 객체 자체에서 keyof typeof obj를 활용하는 흐름으로 설계하세요.
결론: satisfies는 “안전한 검증”과 “좋은 추론”을 동시에 준다
TS 5.x에서 satisfies는 단순 문법 설탕이 아니라, 실무에서 타입 오류를 줄이는 강력한 도구입니다.
as를 줄이고(오류 은폐 방지)- 설정/맵/테이블에서 키 오타·누락을 빠르게 잡고
- 객체 리터럴의 구체 타입 추론을 유지해 자동완성과 리팩터링 품질을 올립니다.
특히 “객체 리터럴을 많이 쓰는 코드베이스”일수록 효과가 큽니다. 인프라/플랫폼 레이어처럼 설정 실수가 장애로 이어질 수 있는 영역이라면 더더욱 그렇습니다. 관련 트러블슈팅 맥락이 궁금하다면 다음 글도 함께 보면 좋습니다: EKS Pod ImagePullBackOff 401 해결 가이드
다음 리팩터링 때부터는 : Type이나 as Type를 붙이기 전에, “이건 강제 캐스팅이 필요한가, 아니면 만족 여부만 확인하면 되는가?”를 먼저 떠올려 보세요. 그 질문의 답이 satisfies인 경우가 생각보다 많습니다.