Skip to main content

Command Palette

Search for a command to run...

TypeScript 배우기 - 11. 조건부 타입

Published
4 min read

조건부 타입은 입력 타입과 출력 타입간의 관계를 설명하는데 도움이 됩니다. 다음은 핸드북에 나온 예제 입니다.

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

// Dog는 Animal 타입으로 할당할 수 있으므로 참. 그러므로 Example1는 number가 됨
type Example1 = Dog extends Animal ? number : string;

// RegExp는 Animal 타입으로 할당할 수 없으므로 거짓. 그러므로 Example2는 string이 됨
type Example2 = RegExp extends Animal ? number : string;

조건부 타입은 다음과 같이 표현됩니다.

SomeType extends OtherType ? TrueType : FalseType;

SomeTypeOtherType으로 할당할 수 있으면 TrueType이 되고 그렇지 않으면 FalseType이 됩니다.

위의 코드는 너무나 간단해서 조건부 타입이 정말로 유용하게 쓰일 수 있는지 알기가 어려운데요, 제네릭과 함께 사용될 때 그 힘이 발휘됩니다. 다음의 코드를 보시죠.

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

앞서 학습했던 오버로드 함수군요. 장황합니다. 이것을 제네릭 함수와 함께 조건부 타입으로 다음과 같이 표현할 수 있습니다.

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

오, 제네릭 인자의 타입에 따라 NameOrId<T>의 타입이 IdLabel 또는 NameLabel로 결정됩니다.

이제 위의 장황한 오버로드 함수를 다음의 간결한 제네릭 함수로 표현할 수 있습니다.

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

// 인자 타입이 string이므로 반환 타입은 `NameLabel`이 됨
let a = createLabel("typescript");

// 인자 타입이 number이므로 반환 타입은 `IdLabel`이 됨
let b = createLabel(2.8);

// `random()`에 의해 인자 타입은 `string | number`가 되므로 반환 타입 역시 `NameLabel | IdLabel`이 됨
let c = createLabel(Math.random() ? "hello" : 42);

조건부 타입의 제약 조건

다음의 코드를 확인해 봅시다.

type MessageOf<T> = T["message"];
> Type '"message"' cannot be used to index type 'T'.

type MessageOf<T>Tmessage 속성 타입을 취하고 있습니다. 하지만 제네릭 타입 T는 어떠한 extends도 가지지 않으므로 message를 알 수 없습니다. 그러므로 오류입니다.

이것을 다음처럼 표현하면 TypeScript는 더이상 오류를 표시하지 않습니다.

// T는 message 속성이 있고 그 타입은 알 수 없다는 것을 표현
type MessageOf<T extends { message: unknown }> = T["message"];

interface Email {
  message: string;
}

// EmailMessageContents는 Email에 `message` 속성이 있으므로 그 타입인 `string`이 됨
type EmailMessageContents = MessageOf<Email>;

하지만 해당 속성이 없을 경우 기본 값으로 naver 타입이 되게 할 수는 없을까요? 이때 조건부 타입을 사용할 수 있습니다.

// message 속성이 있을경우 참이므로 `T["message"]` 타입이 됨. 없을 경우 `never` 타입이 됨
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

// string
type EmailMessageContents = MessageOf<Email>;

// never
type DogMessageContents = MessageOf<Dog>;

다름은 다른 예시 입니다.

type Flatten<T> = T extends any[] ? T[number] : T;

// Extracts out the element type.
type Str = Flatten<string[]>;

// Leaves the type alone.
type Num = Flatten<number>;

Flatten<string[]>any[]로 할당할 수 있으므로 참이 되어 T[number] 타입인 string이 됩니다. Flatten<number>의 경우 any[]로 할당할 수 없으므로 거짓이 되어 T 타입인 number가 됩니다.

조건부 유형 내 추론

Flatten<string[]> 경우 처럼 타입을 추출하는 방식을 보았습니다. 이런 방법처럼 infer 키워드로 관심 있는 타입을 얻을 수 있습니다. 다음의 예시는 ReturnType<T>infer를 이용해서 구현한 것입니다.

// `infer Return`는 타입이 되고 그 타입은 `Return`으로 쓸 수 있음
// 즉, 함수의 반환형을 `Return`으로 취할 수 있게 됨
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

// number
type Num = GetReturnType<() => number>;

// string
type Str = GetReturnType<(x: string) => string>;

// boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>

오버로드 함수의 다중 호출 서명의 경우 가장 포괄적인 서명을 선택합니다.

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;

// string | number
type T1 = ReturnType<typeof stringOrNum>;

분배 조건부 타입

다음의 경우를 살펴봅시다. 타입을 타입[]으로 확장하는 조건부 타입입니다.

type ToArray<Type> = Type extends any ? Type[] : never;

유니온 타입을 사용하면 어떻게 될까요?

type ToArray<Type> = Type extends any ? Type[] : never;

// string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

string | numberToArray<string> | ToArray<number>가 되어 결국에 string[] | number[]이 됩니다.

이 동작은 기본 동작입니다. 만약에 이 동작 대신 (string | number)[]이 되고자 한다면 다음처럼 가능 합니다.

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
20 views

More from this blog

개발, 테스트, 운영에서의 도커 활용

핵심 원칙: "한 번 빌드하고, 어디서든 실행한다 (Build once, run anywhere)" 도커의 가장 큰 장점은 환경 일관성입니다. 동일한 도커 이미지를 사용하여 개발, 테스트, 운영 환경을 구성함으로써 "제 PC에서는 됐는데..." 하는 문제를 최소화할 수 있습니다. 1. 개발 단계 (Development) 목표: 빠른 코드 변경 반영, 쉬운 디버깅, 실제 운영 환경과 유사한 환경 구성. Docker 사용 방안: Dockerf...

May 9, 20256 min read15

[EF Core] 데이터 삭제 시 소프트 삭제 적용

DB에서 데이터를 삭제하면 일반적으로 복구할 수 없습니다. 또한 관계에 따라 영구 삭제 자체가 어려울 수도 있습니다. 그래서 데이터를 영구 삭제하는 대신 IsDeleted 속성을 true로 주고 IsDeleted 속성을 필터링해서 조회하는 방법을 사용하기도 합니다. 이를 소프트 삭제라고 합니다. 그런데 EF에서 알아서 데이터 삭제 시 소프트 삭제를 하고 쿼리시 IsDeleted 속성을 체크해서 삭제한 데이터를 제외한 데이터만 쿼리하게 하는 ...

Mar 18, 20243 min read19

[EF Core] ValueConverter를 이용해서 엔터티 속성의 도메인 관리

EF Core를 사용하면서 문자열 길이 등의 특성을 일일이 지정하는 것은 번거롭습니다. ... [MaxLength(32)] public string? 제목 { get; set; } 엔터티가 한 개일 때는 상관이 없으나 제목 유형이 여러 엔터티에 사용될 경우 유형을 지정하기 번거롭습니다. 속성 유형을 도메인으로 관리하면 참 편할텐데요, ValueConverter를 이용할 수 있습니다. 그런데 이것을 인터페이스 정적 추상를 사용해서 다음처럼 ...

Mar 16, 20242 min read8

디모이 블로그

154 posts

.NET 관련 기술을 선호하고 새로운 언어를 배우는데 관심이 있습니다.