본문으로 건너뛰기

extends를 활용한 조건부 타입(Conditional Types)

질문

TypeScript의 조건부 타입(Conditional Types)은 무엇이며, extends는 어떤 역할을 하나요?

답변 초안

조건부 타입(Conditional Types)은 타입에 조건문을 적용하는 기능입니다.

extends를 이용해 특정 조건을 만족하는지 검사하고, 결과에 따라 서로 다른 타입을 반환할 수 있습니다.

즉,

"이 타입이 특정 타입을 만족하면 A, 아니면 B를 반환한다."

라는 타입 수준의 if 문이라고 생각하면 됩니다.

한 줄 비유 조건부 타입은 "삼항 연산자(? :)의 타입 버전"입니다.

값에서는

isAdmin ? '관리자' : '일반 사용자'

처럼 동작한다면,

타입에서는

T extends string ? A : B

처럼 동작합니다.


조건부 타입이 필요한 이유

어떤 타입이 문자열인지 숫자인지에 따라 반환 타입을 다르게 만들고 싶다고 가정해 보겠습니다.

일반적인 타입 별칭으로는 이런 표현이 어렵습니다.

type Result = string;

항상 같은 타입만 반환하기 때문입니다.

하지만 조건부 타입을 사용하면 입력 타입에 따라 결과를 바꿀 수 있습니다.

type IsString<T> =
T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false

입력된 타입에 따라 다른 타입이 만들어집니다.


조건부 타입 문법

기본 문법은 다음과 같습니다.

type Example<T> =
T extends U ? X : Y;

의미는 다음과 같습니다.

"TU를 만족하면 X, 아니면 Y"

여기서 extends상속이 아니라

"할당 가능한가(assignable)?"

를 검사하는 역할입니다.

예를 들어

type Result =
string extends string ? true : false;

결과는

true

입니다.

반면

type Result =
number extends string ? true : false;

결과는

false

가 됩니다.


extends는 상속이 아니다

많은 사람들이 extends를 보면 클래스의 상속을 떠올립니다.

하지만 조건부 타입에서의 extends는 의미가 다릅니다.

type IsNumber<T> =
T extends number ? true : false;

여기서

T extends number

"Tnumber 타입으로 사용할 수 있는가?"

를 검사하는 것입니다.

즉,

string extends string // true
'hello' extends string // true
1 extends number // true

처럼 타입 호환성(assignability) 을 판단합니다.


실제 예제

문자열이면 문자열 배열을,

그 외에는 원래 타입을 반환한다고 해보겠습니다.

type ToArray<T> =
T extends string ? string[] : T;
type A = ToArray<string>;
// string[]
type B = ToArray<number>;
// number

입력 타입에 따라 결과가 달라집니다.


유니온 타입과 함께 사용하면?

조건부 타입은 유니온 타입과 함께 사용할 때 매우 강력합니다.

type IsString<T> =
T extends string ? true : false;
type Result =
IsString<string | number>;

결과는

true | false

입니다.

왜냐하면 TypeScript는 유니온의 각 타입에 대해 조건을 각각 적용하기 때문입니다.

즉,

IsString<string>

true

그리고

IsString<number>

false

최종적으로

true | false

가 됩니다.

이러한 동작을 분산 조건부 타입(Distributive Conditional Types) 이라고 합니다.


never와 함께 사용하기

조건부 타입은 원하는 타입만 골라내는 데 자주 사용됩니다.

예를 들어 문자열만 남기고 싶다면

type OnlyString<T> =
T extends string ? T : never;
type Result =
OnlyString<
string | number | boolean
>;

분산 조건부 타입이 적용되어

string | never | never

가 되고,

never는 유니온에서 제거되므로

string

만 남습니다.

이 원리를 이용해 TypeScript의 여러 유틸리티 타입이 구현됩니다.


Exclude와 Extract의 구현 원리

대표적인 예시가 ExcludeExtract입니다.

Exclude

type MyExclude<T, U> =
T extends U ? never : T;
type Result =
MyExclude<
string | number | boolean,
number
>;

결과는

string | boolean

입니다.


Extract

type MyExtract<T, U> =
T extends U ? T : never;
type Result =
MyExtract<
string | number | boolean,
number | boolean
>;

결과는

number | boolean

입니다.

실제 TypeScript 내부 구현도 거의 동일한 형태입니다.


NonNullable도 조건부 타입이다

많이 사용하는 NonNullable 역시 조건부 타입으로 구현되어 있습니다.

type MyNonNullable<T> =
T extends null | undefined
? never
: T;
type User =
string | null | undefined;
type Result =
MyNonNullable<User>;

결과는

string

이 됩니다.


infer와 함께 사용하기

조건부 타입에서는 infer를 사용해 타입을 추론할 수도 있습니다.

예를 들어 함수의 반환 타입을 추출하려면

type GetReturnType<T> =
T extends (...args: any[]) => infer R
? R
: never;
function hello() {
return 'hi';
}
type Result =
GetReturnType<typeof hello>;

결과는

string

입니다.

infer는 조건을 만족했을 때 타입을 새롭게 추론해 변수처럼 사용할 수 있게 해주는 키워드입니다.


실무에서 자주 보는 조건부 타입

ReturnType

ReturnType<typeof fetchUser>

함수의 반환 타입을 추출합니다.


Parameters

Parameters<typeof fetchUser>

함수의 매개변수 타입을 튜플로 추출합니다.


Exclude

Exclude<UserRole, 'admin'>

특정 타입을 제거합니다.


Extract

Extract<Event, MouseEvent>

특정 타입만 추출합니다.


NonNullable

NonNullable<User | null>

nullundefined를 제거합니다.

이처럼 TypeScript의 주요 유틸리티 타입 대부분은 조건부 타입을 기반으로 구현되어 있습니다.


조건부 타입과 제네릭의 관계

조건부 타입은 대부분 제네릭과 함께 사용됩니다.

type IsArray<T> =
T extends any[]
? true
: false;

제네릭으로 전달받은 타입을 조건으로 검사하여 새로운 타입을 만드는 것이 조건부 타입의 핵심입니다.


실무 주의점

  • 조건부 타입의 extends상속이 아니라 타입 호환성을 검사하는 연산입니다.
  • 유니온 타입에서는 기본적으로 분산 조건부 타입이 적용됩니다.
  • never를 반환하면 해당 타입은 유니온에서 자동으로 제거됩니다.
  • 대부분의 TypeScript 내장 유틸리티 타입은 조건부 타입과 infer를 활용해 구현되어 있습니다.
  • 조건부 타입이 너무 중첩되면 가독성이 떨어질 수 있으므로 필요한 경우에만 사용하는 것이 좋습니다.

면접 답변

"조건부 타입은 extends를 이용해 타입 수준에서 조건문을 작성하는 기능입니다. T extends U ? X : Y 형태로 사용하며, TU에 할당 가능하면 X, 그렇지 않으면 Y를 반환합니다. 여기서 extends는 상속이 아니라 타입 호환성을 검사하는 역할을 합니다. 조건부 타입은 제네릭과 함께 자주 사용되며, Exclude, Extract, NonNullable, ReturnType 같은 TypeScript의 내장 유틸리티 타입도 이를 기반으로 구현되어 있습니다. 또한 유니온 타입에서는 분산 조건부 타입이 적용되어 각 타입에 대해 조건이 개별적으로 평가됩니다."