정의
타입 계약의 전제 환경은 "이 코드는 컴파일되지 않는다"는 판정이 성립하기 위해 필요한 컴파일러 설정과 버전이에요.
핵심은 이거예요. "컴파일되지 않는다"는 코드의 속성이 아니라 tsconfig의 함수예요. 같은 파일이 어떤 저장소에서는 거절되고 어떤 저장소에서는 통과해요. 그래서 타입으로 무언가를 막았다고 말하려면, 그 판정이 어떤 환경에서 나온 것인지 함께 고정해야 해요.
왜 필요한가
타입 계약을 만들고 "이제 이런 잘못된 코드는 컴파일되지 않아요"라고 말했다고 해볼게요. 그런데 그 말이 참인지는 설정에 달려 있어요.
function greet(name: string) {
return name.toUpperCase();
}
greet(null); // 막힐까요?
strictNullChecks가 꺼져 있으면 통과해요. null이 모든 타입에 대입 가능하거든요. 계약을 아무리 정교하게 짜도, 전제가 없으면 전부 무의미해요.
더 미묘한 경우도 많아요.
const config: Record<string, string> = {};
config.apiUrl.length; // noUncheckedIndexedAccess 없으면 통과해요
type Options = { retry?: number };
const o: Options = { retry: undefined }; // exactOptionalPropertyTypes 없으면 통과해요
그래서 타입 계약을 만드는 저장소에서는 저장소당 한 번 환경을 확인하고 기록해요. 카드마다, 작업마다 다시 하는 게 아니라 한 번이에요.
동작 원리
실효 값이 기준이에요
tsconfig.json 파일에 보이는 내용이 실제 설정이 아니에요. extends 체인을 따라가면서 병합되기 때문이에요.
# 파일에 적힌 값 말고, 실제로 적용되는 값을 봐요
pnpm exec tsc --showConfig
extends: "@company/tsconfig/base" 한 줄 뒤에 무엇이 들어 있는지는 이 명령으로만 알 수 있어요. 프레임워크가 자체 base config를 제공하는 경우(Next.js 등)에도 마찬가지예요.
컴파일러 버전도 실제 resolve된 것이 기준이에요
pnpm exec tsc --version
package.json에 ^5.4.0 이라고 적혀 있어도 lockfile이 실제로 무엇을 설치했는지가 판정 기준이에요. TypeScript Playground나 최신 버전에서만 통과한 결과는 증거가 아니에요. 에디터가 쓰는 버전(VS Code 내장 TypeScript)이 프로젝트 버전과 다른 경우도 흔해요.
확인할 항목
| 항목 | 기준 | 미충족 시 약해지는 것 |
|---|---|---|
| TypeScript 버전 | ≥ 5.4 | NoInfer(5.4) · const 타입 파라미터(5.0) · satisfies(4.9)를 못 써요 |
strict | 필수 | union 좁히기와 null 안전이 없어서 계약의 전제 자체가 없어요 |
strictFunctionTypes | 권장 | 콜백 파라미터 검사가 느슨해져요 (메서드 이변성 예외는 켜도 남아요) |
useUnknownInCatchVariables | 권장 | catch 값이 any처럼 흘러다녀요 |
noUncheckedIndexedAccess | 권장 | 배열 인덱스·lookup 접근이 undefined 검사 없이 통과해요 |
noPropertyAccessFromIndexSignature | 권장 | 열린 dictionary 키를 확정 속성처럼 읽어요 |
exactOptionalPropertyTypes | 권장 | "유지 vs 삭제"의 undefined 구분이 보증되지 않아요 |
strict는 여러 플래그의 묶음이에요. 개별 플래그를 끄면서 strict: true를 유지하는 저장소도 있으니, 묶음 이름이 아니라 실효 값을 봐요.
실무 적용
판정과 대응
전부 충족 — tsconfig 위치와 TypeScript 버전을 기록하고 진행해요. 이후 작업에서 다시 검증하지 않아요.
strict 또는 버전이 미충족 — 여기서 멈춰요. tsconfig를 조용히 바꾸지 않아요. 저장소 전체에 파급되는 정책 변경이라, 켜는 순간 기존 파일 수백 개에서 오류가 터질 수 있어요. 미충족 항목과 영향 범위를 정리해서 사람에게 결정을 넘겨요.
권장 플래그가 미충족 — 켜는 변경을 제안하되, 거절되거나 보류되면 약해지는 계약 목록을 기록하고 진행해요. "이건 컴파일이 아니라 리뷰와 테스트가 잡아야 한다"는 뜻이니까요.
기록해 둘 것
## 타입 환경 (2026-08-24 확인)
- TypeScript: 5.6.3 (lockfile resolve 기준)
- tsconfig 실효값: `tsc --showConfig` 기준
- strict: true
- strictFunctionTypes: true
- noUncheckedIndexedAccess: true
- exactOptionalPropertyTypes: **false** ← 미충족
- 약해지는 계약: 연산 union의 "유지 vs 삭제" 구분이 컴파일로 보증되지 않음
→ 해당 mutation 경로는 런타임 테스트가 소유
컴파일러 업그레이드는 정책 변경이에요
TypeScript 버전을 올리는 건 단순한 도구 업데이트가 아니에요. assignability 판정과 strict 계열 동작이 바뀌면 이전에 통과하던 코드가 거절되거나 그 반대가 될 수 있어요. 기록해 둔 환경과 다른 버전이 diff에 나타나면 이 확인을 다시 하고 기록을 갱신해요.
성능 증거가 필요한 경우
재귀 타입이나 분배 conditional 타입을 도입하면 컴파일 시간이 눈에 띄게 늘 수 있어요. 이때는 숫자 한계를 문서에 하드코딩하지 말고 실제 프로젝트의 컴파일러 진단으로 판정해요.
pnpm exec tsc --noEmit --extendedDiagnostics
도입 전후 값을 비교해서 기록해요. --generateTrace는 악화가 실제로 관측됐고 원인 규명이 필요할 때만 써요.
트레이드오프
한 번의 확인 비용이 들어요. 명령 두 개를 돌리고 표를 채우는 일이라 10분이면 끝나지만, 급할 때는 건너뛰고 싶어져요.
건너뛰면 무슨 일이 생기냐면 — 타입 계약을 열심히 만들었는데 나중에 strict가 꺼져 있다는 걸 발견해요. 그동안 "막았다"고 믿었던 것들이 전부 안 막혔던 거죠. 이건 되돌리기가 비싸요.
플래그를 켜는 건 더 비싸요. noUncheckedIndexedAccess 하나만 켜도 기존 코드에서 수백 개 오류가 날 수 있어요. 그래서 "권장 플래그가 꺼져 있으면 무조건 켠다"가 아니라, 켜는 걸 제안하고 거절되면 약해지는 것을 기록한다가 현실적인 절충이에요.
사용하면 안 되는 경우
- 타입 계약을 만들지 않는 저장소. 스크립트나 프로토타입에 이 절차를 적용하면 형식만 늘어요.
- 매 작업마다 반복하기. 저장소당 한 번이에요. tsconfig나 TypeScript 버전이 바뀌는 diff에서만 다시 봐요.
- 환경을 고친다는 이유로 사람 승인 없이 tsconfig를 수정하기. 이건 절대 하지 않아요. 영향 범위가 저장소 전체예요.
흔한 실수
tsconfig.json파일만 보고 판단해요.extends체인 뒤에 무엇이 있는지 모른 채 결론을 내려요.- 에디터 기준으로 확인해요. VS Code 내장 TypeScript 버전과 프로젝트 lockfile 버전이 다르면, 에디터에서는 빨간데 CI는 통과하거나 그 반대가 돼요.
- Playground에서 검증해요. Playground는 항상 최신 버전에 기본 설정이에요. 저장소 판정의 증거가 될 수 없어요.
strict: true만 보고 안심해요. 개별 플래그가 뒤에서 꺼져 있을 수 있어요.- 미충족 플래그를 조용히 켜요. 오류 수백 개와 함께 리뷰어를 놀라게 해요.
- 약해지는 계약을 기록하지 않아요. 플래그를 못 켠 건 괜찮은데, 그래서 무엇이 안 막히는지 아무도 모르는 게 문제예요.
관련 개념
- type-level-testing — 이 환경 위에서 "무엇이 컴파일되지 않는가"를 코드로 고정하기
- false-type-contracts — 플래그가 꺼져 있어서 거짓이 되는 계약들
- typescript-variance —
strictFunctionTypes를 켜도 남는 구멍