TypeScript는 의도적으로 100% 타입 안전성(soundness)보다 실용성을 선택 했습니다. 이 글에서는 타입 시스템이 잡지 못하는 경우와 보완 방법을 정리합니다.

TypeScript의 설계 철학

TypeScript 팀의 디자인 목표에는 다음이 명시되어 있습니다:

"완전히 건전하거나(sound) 증명 가능한 타입 시스템을 목표로 하지 않는다"

이는 TypeScript가 JavaScript 생태계와의 호환성을 위해 일부 타입 안전성을 의도적으로 포기 했음을 의미합니다.

알려진 Unsoundness

1. 배열의 공변성

TYPESCRIPT
class Animal { name = 'animal'; }
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }

// Dog[]을 Animal[]에 할당 가능 (공변적)
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs;

// ⚠️ Cat을 Dog 배열에 추가할 수 있음!
animals.push(new Cat());

// dogs[1]은 실제로 Cat이지만 TypeScript는 Dog로 인식
dogs[1].bark(); // 런타임 에러: bark is not a function

TypeScript는 편의를 위해 배열의 변경 가능성을 무시합니다. ReadonlyArray를 사용하면 이 문제를 방지할 수 있습니다.

2. 타입 단언의 위험

TYPESCRIPT
interface User {
  name: string;
  age: number;
}

// 빈 객체를 User로 단언 — 에러 없음
const user = {} as User;
console.log(user.name.toUpperCase()); // 런타임 에러!

3. any의 전염

TYPESCRIPT
function getConfig(): any {
  return JSON.parse('{"port": 3000}');
}

const config = getConfig();
const port: number = config.port;    // OK
const name: string = config.port;    // ⚠️ OK — any이므로 타입 검사 안 됨

4. 인덱스 접근의 undefined

TYPESCRIPT
// noUncheckedIndexedAccess가 없으면
const arr = [1, 2, 3];
const value: number = arr[10]; // ⚠️ undefined인데 number로 취급

noUncheckedIndexedAccess: true로 이 문제를 완화할 수 있습니다.

5. 함수 매개변수의 이변성 (메서드)

TYPESCRIPT
interface EventTarget {
  addEventListener(type: string, handler: (event: Event) => void): void;
}

// 메서드 시그니처는 strictFunctionTypes에서도 이변적
const target: EventTarget = {
  addEventListener(type, handler) {
    // handler에 MouseEvent를 전달해도 에러 없음
  }
};

6. 타입 가드의 거짓말

TYPESCRIPT
// 타입 가드 함수가 잘못된 검증을 해도 에러 없음
function isString(value: unknown): value is string {
  return typeof value === 'number'; // ⚠️ 잘못된 검증인데 컴파일 OK
}

const value: unknown = 42;
if (isString(value)) {
  // value: string으로 좁혀졌지만 실제로는 number
  value.toUpperCase(); // 런타임 에러
}

구조적 타이핑의 한계

TYPESCRIPT
// 같은 구조면 호환 — 의미적 구분 불가
type Celsius = number;
type Fahrenheit = number;

function boil(temp: Celsius) { /* ... */ }

const f: Fahrenheit = 212;
boil(f); // ⚠️ 에러 없음 — 화씨를 섭씨 위치에 전달

보완: Branded Types

TYPESCRIPT
type Celsius = number & { __brand: 'Celsius' };
type Fahrenheit = number & { __brand: 'Fahrenheit' };

function boil(temp: Celsius) { /* ... */ }

const f = 212 as Fahrenheit;
// boil(f); // ❌ Error — 브랜드가 다름

런타임 타입 부재

TYPESCRIPT
// TypeScript 타입은 런타임에 존재하지 않음
interface User {
  name: string;
  age: number;
}

function processUser(data: unknown) {
  // ❌ 이렇게 할 수 없음
  // if (data instanceof User) { ... }

  // ✅ 런타임 검증 필요
  if (
    typeof data === 'object' &&
    data !== null &&
    'name' in data &&
    'age' in data
  ) {
    // 수동 검증...
  }
}

보완: Zod, io-ts 등 런타임 스키마 검증 라이브러리

TYPESCRIPT
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

function processUser(data: unknown) {
  const result = UserSchema.safeParse(data);
  if (result.success) {
    // result.data는 타입 안전
  }
}

TypeScript가 잡지 못하는 것들

문제원인보완
배열 공변성의도적 unsoundnessReadonlyArray 사용
any 전염타입 탈출구ESLint no-explicit-any
인덱스 undefined기본값이 관대함noUncheckedIndexedAccess
타입 가드 거짓말개발자 책임테스트로 검증
런타임 타입 부재Type ErasureZod, io-ts
의미적 구분 불가구조적 타이핑Branded Types
외부 데이터 검증타입은 컴파일 타임만Zod, class-validator

TypeScript가 의도적으로 지원하지 않는 것

  • Nominal Typing: 이름 기반 타입 구분 (Branded Types로 흉내)
  • ** 완전한 의존 타입(Dependent Types)**: 값에 따라 타입이 변하는 것
  • Effect Tracking: 함수의 부수 효과를 타입으로 추적 (Effect 라이브러리)
  • **throws 절 **: 함수가 던지는 에러 타입 선언

마무리: TypeScript의 가치

한계가 있음에도 TypeScript는 JavaScript 생태계에서 최선의 타입 안전 도구 입니다.

  • **IDE 지원 **: 자동 완성, 리팩터링, Go to Definition
  • ** 팀 협업 **: 코드의 의도를 타입으로 문서화
  • ** 버그 예방 **: 런타임 에러의 상당수를 컴파일 타임에 잡음
  • ** 점진적 도입 **: JavaScript와 호환되어 서서히 전환 가능

면접 포인트: "TypeScript의 한계를 알고 있나요?"라는 질문에 "TypeScript는 의도적으로 soundness보다 실용성을 선택했으며, 배열 공변성이나 런타임 타입 부재 같은 한계가 있지만 Branded Types, Zod 등으로 보완할 수 있습니다"라고 답하면 됩니다.

정리

  • TypeScript는 100% 타입 안전성보다 실용성과 JavaScript 호환을 우선한다
  • 배열 공변성, any 전염, 타입 가드 거짓말 등 알려진 unsoundness가 있다
  • strict 모드, noUncheckedIndexedAccess 등으로 안전성을 높일 수 있다
  • 런타임 타입이 필요하면 Zod 같은 스키마 검증 라이브러리를 사용한다
  • Branded Types로 구조적 타이핑의 한계를 보완할 수 있다
  • 한계를 알고 보완하면서 쓰는 것이 TypeScript를 제대로 활용하는 방법이다
댓글 로딩 중...