# Til: 타입스크립트의 이상한 동작



삽질하다가 뭐가 문제인지 깨닫고 잠깐 쉴 겸 쓰는 글이다.

`stc`에 `Awaited` 타입 지원을 추가하기 위해 작업하고 있었다.

```ts
//@strict: true

export type Foo = Awaited<Promise<{ x: number }>>;

declare var foo: Foo;
var foo = { x: 1 };
```

위와 같은 간단한 테스트 케이스를 만들고 디버깅을 했다.
그런데 디버깅을 하면 할수록 뭔가 이상했다.
최종 결과엔 분명 오류가 있는데 각 함수 반환 값을 찍어보면 다 정상적이었다.
그래서 테스트를 잘게 쪼개기 시작했다.

```ts
export type T2 = Promise<{ x: number }> extends object & {
  then(onfulfilled: infer F, ...args: infer _): any;
} // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
  ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
    ? V
    : never // the argument to `then` was not callable
  : Promise<{ x: number }>; // non-object or non-thenable:

declare var foo: T2;
declare var foo: { x: number };
```

그리고 이게 `stc` 가 실패하는 테스트 중 하나였다.
`extends`가 2개 있어서 디버깅이 힘드니 이것도 쪼개기로 했다.
겸사겸사 `F`의 타입도 알아낼 겸 아래와 같은 테스트를 작성했다.

```ts
export type T2 = Promise<{ x: number }> extends object & {
  then(onfulfilled: infer F, ...args: infer _): any;
}
  ? F
  : never;

declare var foo: T2;
declare var foo: ((value: { x: number }) => unknown) | null | undefined;
```

`tsc`가 이 테스트 케이스에 대해 성공하고, `extends`가 참인 것은 자명하므로, `F`의 타입은 `((value: { x: number }) => unknown) | null | undefined;`이다.
이제 이 타입을 복사해서 인라이닝하면 뭐가 문제인지 나오겠지싶었다.

```ts
export type T2 = Promise<{ x: number }> extends object & {
  then(onfulfilled: infer F, ...args: infer _): any;
} // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
  ? ((value: { x: number }) => unknown) | null | undefined extends (
      value: infer V,
      ...args: infer _
    ) => any // if the argument to `then` is callable, extracts the first argument
    ? V
    : never // the argument to `then` was not callable
  : Promise<{ x: number }>; // non-object or non-thenable:

declare var foo: T2;
declare var foo: { x: number };
```

위의 파일이 `F`를 인라이닝한 값인데, 이상한 결과가 나왔다.
`tsc`가 실패하더라.
그래서 잠깐 고민하다가 `| null | undefined`를 지워봤다.

```ts
export type T2 = Promise<{ x: number }> extends object & {
  then(onfulfilled: infer F, ...args: infer _): any;
} // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
  ? ((value: { x: number }) => unknown) extends (
      value: infer V,
      ...args: infer _
    ) => any // if the argument to `then` is callable, extracts the first argument
    ? V
    : never // the argument to `then` was not callable
  : Promise<{ x: number }>; // non-object or non-thenable:

declare var foo: T2;
declare var foo: { x: number };
```

이 코드는 타입 검사를 성공하더라.
이건... `infer F`로 추론된 타입이랑 사용자가 직접 입력한 타입이랑 `extends` 키워드의 동작이 다르다는 소리다.
타입 시스템이 이런 식으로 동작한다는 게 어지럽긴 한데 해결은 쉬우니까 이만 줄이겠다.

