메인 콘텐츠로 이동

2026-03-24

TypeScript 컴파일러 크래시 버그를 고쳐서 PR을 올린 이야기 (#63005)

발단: "Help Wanted" 이슈 하나

TypeScript GitHub 레포에서 #63005 이슈를 발견했다. Crash, Bug, Help Wanted 라벨이 붙어 있고, 아무에게도 할당되지 않은 상태였다.

이슈의 내용은 이랬다. tuple 타입에서 middle rest element와 trailing variadic element를 함께 사용해 추론할 때, 컴파일러가 크래시한다는 것.

// 이 코드가 컴파일러를 크래시시킨다
type SubTup2RestAndTrailingVariadic2<T extends unknown[]> = T extends [
    ...(infer C)[],
    ...infer B extends [any, any, crash]
]
    ? [C, ...B]
    : never;
 
type Test = SubTup2RestAndTrailingVariadic2<[...a: 0[], b: 1, c: 2]>;

에러 메시지도 없이, 그냥 TypeError: Cannot read properties of undefined (reading 'aliasSymbol')로 프로세스가 죽어버린다. 기대되는 동작은 crash라는 이름을 찾을 수 없다는 syntax error를 보여주는 것이다.

크래시 재현

먼저 TypeScript 레포를 클론하고 빌드한 뒤, 재현 코드를 실행했다.

node built/local/tsc.js --noEmit test_repro.ts

스택 트레이스가 정확히 이슈에 보고된 것과 일치했다.

TypeError: Cannot read properties of undefined (reading 'aliasSymbol')
    at inferFromTypes (_tsc.js:68655:18)
    at inferFromObjectTypes (_tsc.js:69137:21)
    at invokeOnce (_tsc.js:68847:9)
    at inferFromTypes (_tsc.js:68809:11)

Root Cause 분석

스택 트레이스를 따라 src/compiler/checker.tsinferFromTypes 함수를 열었다. 이 함수의 첫 번째 줄에서 source.aliasSymbol에 접근하는데, sourceundefined로 들어오면서 크래시가 발생하고 있었다.

호출부인 inferFromObjectTypes를 추적했다. tuple inference 로직에서 [...T, ...rest][...rest, ...T] 두 가지 패턴을 처리하는 분기가 있었는데, 둘 다 getElementTypeOfSliceOfTupleType()의 반환값을 ! (non-null assertion)으로 처리하고 있었다.

// 문제의 코드 (수정 전)
inferFromTypes(
  getElementTypeOfSliceOfTupleType(source, startLength + impliedArity, endLength)!,
  elementTypes[startLength + 1]
);

핵심은 이것이다: getElementTypeOfSliceOfTupleType()은 빈 슬라이스일 때 undefined를 반환할 수 있다.

재현 코드에서 source tuple [...a: 0[], b: 1, c: 2]는 고정 원소가 3개이고, variadic constraint [any, any, any]의 implied arity도 3이다. rest 부분에 할당할 원소가 남지 않아 빈 슬라이스가 되고, undefined가 반환된다. 이 undefined!를 뚫고 inferFromTypes에 전달되면서 크래시.

재밌는 건, 바로 아래에 있는 single-rest-element 분기에서는 이미 null check를 하고 있었다는 점이다. 즉, 이건 처리가 누락된 케이스였다.

수정

수정은 간단했다. 두 분기 모두 getElementTypeOfSliceOfTupleType()의 반환값을 null check한 후에만 inferFromTypes를 호출하도록 변경했다.

// [...T, ...rest] 분기
const restType = getElementTypeOfSliceOfTupleType(
  source, startLength + impliedArity, endLength
);
if (restType) {
  inferFromTypes(restType, elementTypes[startLength + 1]);
}
 
// [...rest, ...T] 분기
const restType = getElementTypeOfSliceOfTupleType(
  source, startLength, endLength + impliedArity
);
if (restType) {
  inferFromTypes(restType, elementTypes[startLength]);
}

바로 아래에 있는 기존 코드의 패턴을 그대로 따른 것이다. 일관성 있는 수정.

테스트

기존 테스트 파일인 inferTypesWithFixedTupleExtendsAtVariadicPosition.ts에 크래시를 재현하는 테스트 케이스를 추가했다.

// repro #63005 - implied arity가 source tuple 전체를 소비할 때 크래시하지 않아야 함
type SubTup2RestAndTrailingVariadic3<T extends unknown[]> = T extends [
    ...(infer C)[],
    ...infer B extends [any, any, any]
]
    ? [C, ...B]
    : never;
 
type SubTup2RestAndTrailingVariadic3Test =
  SubTup2RestAndTrailingVariadic3<[...a: 0[], b: 1, c: 2]>;
// 결과: never (rest 부분이 비어있으므로)

결과 타입이 never로 평가되는 것이 올바르다. source tuple의 3개 원소가 모두 variadic constraint에 소비되어 rest에 남는 게 없기 때문이다.

모든 관련 테스트를 돌렸다:

  • inferTypesWithFixedTupleExtendsAtVariadicPosition - 6개 테스트 통과
  • variadic 관련 테스트 전체 - 20개 통과
  • inferTypes 관련 테스트 전체 - 42개 통과
  • tuple 관련 테스트 전체 - 231개 통과

regression 없음을 확인하고 PR을 올렸다.

PR 제출과 CLA 서명

PR #63288을 올렸다. 외부 기여자 PR이다 보니 몇 가지 절차가 있었다:

  • CLA(Contributor License Agreement) 서명 - Microsoft의 CLA에 동의해야 한다. PR 코멘트에 @microsoft-github-policy-service agree를 입력하면 된다.
  • CI 워크플로우 승인 대기 - 외부 기여자의 PR은 보안상 메인테이너가 수동으로 CI를 승인해야 실행된다.
  • 코드 리뷰 대기 - TypeScript 팀의 리뷰를 기다리는 중이다.

배운 것

TypeScript 컴파일러 코드는 checker.ts 하나가 수만 줄이지만, 스택 트레이스를 따라가면 생각보다 문제 지점을 빠르게 좁힐 수 있다. 이번 수정은 딱 2줄의 null check 추가였지만, 그 2줄에 도달하기까지의 과정이 핵심이었다:

  1. 크래시를 재현한다
  2. 스택 트레이스에서 crash site를 찾는다
  3. undefined가 어디서 흘러들어오는지 역추적한다
  4. 주변 코드의 패턴을 참고해서 일관된 방식으로 수정한다
  5. 기존 테스트 파일에 케이스를 추가하고 regression이 없는지 확인한다

"Help Wanted" 라벨이 붙은 이슈는 메인테이너가 외부 기여를 환영한다는 신호다. 크래시 버그는 재현이 쉽고, 수정의 정확성을 검증하기도 명확해서 첫 기여로 좋은 선택이었다.

관련 글

로딩 중...