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;
-
Shorthand 문법을 사용하여 이변성을 갖습니다.
-
Shorthand 문법을 풀고 기존 문법으로 치환해봅시다.
{ key: value }['key'] = value;
형식입니다.
-
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'];