Search

TypeScript의 in, out 변성 어노테이션 이해하기

subtitle
Tags
typescript
Created
2025/02/10
2 more properties

배경

Apollo Client 오픈소스 코드 중 MockedResponse에서 in, out이라는 TypeScript 키워드를 처음 보게 되었다. TypeScript를 많이 사용했음에도 불구하고 이 키워드는 처음보는 거라 궁금해서 공부하게 되었다.
export interface MockedResponse< // @ts-ignore out TData = Record<string, any>, out TVariables = Record<string, any>, > { request: GraphQLRequest<TVariables>; maxUsageCount?: number; result?: | FetchResult<Unmasked<TData>> | ResultFunction<FetchResult<Unmasked<TData>>, TVariables>; error?: Error; delay?: number; variableMatcher?: VariableMatcher<TVariables>; newData?: ResultFunction<FetchResult<Unmasked<TData>>, TVariables>; }
TypeScript
복사
TypeScript의 제네릭에서 in, out 키워드는 타입의 변성(variance)을 나타낸다.

변성이란?

타입의 변성(variance)은 타입 계층에서 제네릭 타입이 어떻게 변화할 수 있는지를 설명하는 개념이다. TypeScript는 기본적으로 변성을 자동으로 처리하지만, 변성 어노테이션(in, out)을 사용하면 제네릭 타입의 의도를 명확히 드러낼 수 있다.

변성의 종류

1.
공변성 (Covariance) → out 키워드 사용
2.
반공변성 (Contravariance) → in 키워드 사용
3.
무공변성 (Invariant) → in out 키워드 사용

공변성(out)이란?

공변성은 타입의 생산자(producer) 역할을 하는 경우 사용된다.
즉, 제네릭 타입이 리턴값에 사용될 때 적용된다.
ex) A가 B 타입을 포함하면, T<A> 자리에 T<B>를 사용할 수 있음

예시 코드

interface Producer<out T> { produce(): T; } const stringProducer: Producer<string> = { produce: () => "Hello", }; const genericProducer: Producer<string | number> = stringProducer; // ✅ 가능 (공변적 관계)
TypeScript
복사
위 코드에서 Producer<string>Producer<string | number>에 할당할 수 있다. stringstring | number의 부분 집합이기 때문이다.

반공변성(in)이란?

반공변성은 타입의 소비자(consumer) 역할을 하는 경우 사용된다.
즉, 제네릭 타입이 함수의 인자로 사용될 때 적용된다.
ex) A가 B타입을 포함하면, T<B> 자리에 T<A>를 사용할 수 있음

예시 코드

interface Consumer<in T> { consume(arg: T): void; } const logNumber = (arg: number) => console.log(arg); const logNumberOrString: Consumer<number | string> = { consume: logNumber }; const log: Consumer<number> = logNumberOrString; // ✅ 가능 (반공변적 관계) // const log2: Consumer<number | string> = log; // ❌ 불가능
TypeScript
복사
위 코드에서 Consumer<number | string>Consumer<number>에 할당할 수 있다.
logNumberOrStringnumber | string을 처리할 수 있으므로 number를 받아야 하는 곳에서 사용될 수 있다.
하지만 반대로 logNumberstring을 처리할 수 없으므로 Consumer<number>Consumer<number | string>에 할당할 수 없다.

무공변성(in out)이란?

때로는 제네릭 타입이 함수의 입력값(consumer)과 출력값(producer) 모두에서 사용될 때가 있다. 이 경우, in out 어노테이션을 사용할 수 있다.

예시 코드

interface ProducerConsumer<in out T> { consume: (arg: T) => void; make(): T; }
TypeScript
복사
이 인터페이스에서는 Tconsume의 파라미터 타입으로 사용되면서 동시에 make()의 반환 타입으로 사용된다. 따라서 T무공변적(invariant)이다.

변성 어노테이션을 사용하는 이유

1.
타입 의도를 명확히 표현할 수 있다. (예: 리턴값이면 out, 파라미터면 in)
2.
타입 안정성을 높일 수 있다. (예: 잘못된 타입 변성을 막음)

사용 시 주의할 점

타입이 구조적으로 일치할 때만 사용해야 한다.
예를 들어 out T라고 지정한 후, string | number를 제네릭으로 사용하고 number를 반환하면 string | number !== number가 되어 구조적으로 일치하지 않으므로 변성 어노테이션이 무시될 수 있다.

참고 자료