김사람 블로그
thumbnail
TypeScript 타입 변성(Variance)
TS
2022.09.20.

Variance

타입 시스템에는 변성(Variance)의 개념이 존재합니다.

기저 타입(Base type)은 같지만, 타입 인자(Type argument)가 다른, 타입 간 호환성에 관한 개념입니다.

종류는 공변성(Covariance), 반공변성(Contravariance), 이변성(Bivariance), 불변성(Invariance)이 있습니다.


또한 타입 시스템에는 Subtyping이라는 타입 간 관계의 개념이 존재합니다.

A가 B를 대체할 수 있다면, A는 B의 Subtype이 되고, B는 A의 Supertype이 됩니다. (표기 : A <: B)



  • 공변성(Covariance)

타입 간 관계(Subtype)가 유지되는 성질입니다.

ex) A가 B의 서브타입이면, F<A>F<B>의 서브타입.

A <: B  =>  F<A> <: F<B>

  • 반공변성(Contravariance)

타입 간 관계(Subtype)가 역전되는 성질입니다.

ex) A가 B의 서브타입이면, F<B>F<A>의 서브타입.

A <: B  =>  F<A> :> F<B>

  • 이변성(Bivariance)

공변성과 반공변성을 모두 갖는, 타입 간 호환이 자유로운 성질입니다.


  • 불변성(Invariance)

타입 간 관계(Subtype)가 존재하지 않아서, 동일한 타입이 아니면 타입 간 호환이 불가한 성질입니다.


In TypeScript

Structural type system

타입스크립트는 구조적 타입 시스템(Structural type system / Structural Subtyping)을 사용합니다.

이름이나 상속과 관계 없이, 타입의 내부 구조에 의해 타입 간 관계, 호환성이 결정됩니다.

따라서 Subtyping의 대체 가능성을 타입의 구조로 판단합니다.



strictFunctionTypes
class Animal {
  animal: undefined;
}
class Dog {
  animal: undefined;
  dog: undefined;
}
  • 여기서 Dog가 Animal을 대체할 수 있으므로, Dog는 Animal의 서브타입입니다. Dog <: Animal

다음은 두 타입을 제네릭으로 사용하는 세 가지 경우입니다.


1. 배열 값의 타입으로 사용하는 경우
let arr1: Array<Animal> = [];
let arr2: Array<Dog> = [];
arr1 = arr2; // Ok
arr2 = arr1; // Error with --strictFunctionTypes
  • 타입 간 관계가 유지됩니다. 즉, 공변성(Covariance)을 가집니다. Array<Dog> <: Array<Animal>



2. 함수 반환값의 타입으로 사용하는 경우
type F1<T> = () => T;
let f1: F1<Animal> = () => new Animal();
let f2: F1<Dog> = () => new Dog();
f1 = f2; // Ok
f2 = f1; // Error with --strictFunctionTypes
  • 타입 간 관계가 유지됩니다. 즉, 공변성(Covariance)을 가집니다. F1<Dog> <: F1<Animal>



3. 함수 매개변수의 타입으로 사용하는 경우
type F2<T> = (x: T) => void;
let f3: F2<Animal> = (x: Animal) => {};
let f4: F2<Dog> = (x: Dog) => {};
f3 = f4; // Error with --strictFunctionTypes
f4 = f3; // Ok
  • 타입 간 관계가 역전됩니다. 즉, 반공변성(Contravariance)을 가집니다. F2<Dog> :> F2<Animal>



타입스크립트의 타입은 기본적으로 공변성을 갖지만, 함수의 매개변수로 사용할 때 예외적으로 반공변성을 가집니다.

즉, 타입이 출력에 사용되면 공변, 입력에 사용되면 반공변합니다.


타입스크립트 컴파일러는 "strictFunctionTypes": true인 경우, 이렇게 변성에 의한 타입을 검사합니다. (tsconfig에서 설정)

그리고 strictFunctionTypes를 활성화하지 않아도 변성을 검사할 수 있는 다른 방법이 존재합니다.



Optional Variance Annotations

TypeScript 4.7에서 도입된 Optional Variance Annotation을 사용하여, 매개변수의 변성을 명시할 수 있습니다.

type F1<out T> = () => T;
  • 공변성의 표기로 해당 타입 앞에 out을 사용할 수 있습니다.
type F2<in T> = (x: T) => void;
  • 반공변성의 표기로 해당 타입 앞에 in을 사용할 수 있습니다.
interface FF<in out T> {
  f1: () => T;
  f2: (x: T) => void;
}
  • 불변성의 표기로 해당 타입 앞에 in out을 사용할 수 있습니다.

다른 변성을 표기한다고 해서 변성이 바뀌지 않습니다. 오히려 에러만 뜹니다.

변성을 명시함으로써 독자가 매개변수의 용도를 알기 쉽게 하고, 컴파일 과정 중 타입 검사 속도를 높이는 역할을 합니다.


Bivariance

공변성과 반공변성을 모두 갖는, 이변성(Bivariance) 이 필요한 경우,


1. strictFunctionTypes 비활성화

strictFunctionTypes를 비활성화하면, 컴파일러는 변성 타입 검사를 하지 않으므로 변성과 관계없이 타입을 사용할 수 있습니다.

물론 Optional Variance Annotation이 사용되었다면, 해당 변성에 맞게 타입을 사용해야 합니다.



2. Shorthand for Method definition
interface Example<T> {
  f1: (x: T) => void; // Contravariance
  f2(x: T): void; // Bivariance
}
  • 메서드의 타입을 정의할 때, ES6에서 도입된 Shorthand 문법을 사용하면 해당 메서드의 매개변수는 이변성을 갖습니다.



3. bivarianceHack
type Example<T> = { bivarianceHack(x: T): void }['bivarianceHack'];
  • 함수의 타입을 정의할 때, 위와 같은 형식으로 작성하면 해당 함수의 매개변수는 이변성을 갖습니다.

원리는 아래와 같습니다.

1. type Example<T> = { bivarianceHack(x: T): void }['bivarianceHack'];
2. type Example<T> = { bivarianceHack: (x: T) => void }['bivarianceHack'];
3. type Example<T> = (x: T) => void;
  1. Shorthand 문법을 사용하여 이변성을 갖습니다.

  2. Shorthand 문법을 풀고 기존 문법으로 치환해봅시다.

    • { key: value }['key'] = value; 형식입니다.
  3. key가 일치하므로 value인 함수가 도출됩니다.


bivarianceHack은 리액트 타입에서 쓰이고 있습니다.

// index.d.ts
type RefCallback<T> = { bivarianceHack(instance: T | null): void }['bivarianceHack'];
type EventHandler<E extends SyntheticEvent<any>> = {
  bivarianceHack(event: E): void;
}['bivarianceHack'];

참고 링크

NAEFLIX
← 다음 글
Debounce & Throttle 함수 만들기
이전 글 →
We can love completely without complete understanding.
- Norman Maclean, A River Runs Through it.