공변성(Covariance) : A가 B의 서브타입이면, T<A>는 T<B>의 서브타입이다.
반공변성(Contravariance) : A가 B의 서브타입이면, T<B>는 T<A>의 서브타입이다.
이변성(Bivariance) : A가 B의 서브타입이면, T<A> → T<B>도 되고 T<B> → T<A>도 되는 경우
불변성(immutability) : A가 B의 서브타입이더라도, T<A> → T<B>도 안 되고 T<B> → T<A>도 안 되는 경우
뭔소린지 모르겠다. 일단 코드로 용어 하나하나 정리해보면,
let stringArray: Array<string> = [];
let array: Array<string | number> = [];
array = stringArray; // OK
stringArray = array; // Error
let subObj: { a: string; b: number } = { a: '1', b: 1};
let superObj: { a: string | number; b: number } = subObj; // superObj는 subObj 포함
우선 기본적으로 생 string은 유니온 string | number의 서브타입 / 그리고, Array<string>은 Array<string | number>의 서브 타입이 된다. 그리고 { a: string; b: number }도 { a: string | number; b: number } 의 서브 타입이 된다.
이렇게 A가 B의 서브타입이면 T<A>가 T<B>의 서브타입이 되면 T를 공변적이라고 부를 수 있다.
이를 조건부 타입으로 정의해보면
// 조건부 타입: T가 P에 속해있으면 ? true : false
type IsSubtypeOf<T, P> = T extends P ? true : false;
type T1 = IsSubtypeOf<Array<string>>, Array<string | number>>; // true
type T2 = IsSubtypeOf<Array<string | number>, Array<string>>; // false
type T3 = IsSubtypeOf<{ a: string; b: number}, { a: string | number; b: number }>; // true
type T4 = IsSubtypeOf<{ a: string | number; b: number}, { a: string; b: number }>; // false
반공병성 코드를 보면
type Logger<T> = (param: T) => void;
let logNumber: Logger<number> = (param) => {
console.log(param); // number
};
let log: Logger<string | number> = (param) => {
console.log(param);
}
logNumber = log; // OK
log = logNumber; // Error
// 기본적으로 number <; string | number 지만, 함수 매개변수 제네릭에서는 거꾸로>
// Logger<string | number> <: Logger<number>>
위의 코드에서 string | number 를 받는 log 함수와, 인자로 number 만을 받는 logNumber 함수가 있다.
원래대로면 공변성의 규칙에 따라 log 함수는 넓은 타입이고, logNumber는 좁은 타입이라서 log = logNumber식이 성립되야 하지만 에러가 발생한다. 함수의 매개변수 타입을 다룰경우에는 반공변성을 따르기 때문이다.
따라서 logNumber=. log가 성립된다.
아래 코드에서 함수의 return 타입을 보면 b가 a보다 넓은 타입이다.
좁은 타입을 넓은 타입에 대입할 수 있어서 number c number | string 즉 a c b가 된다. 공변성
// 매개변수 타입은 같고, 리턴값 타입이 다를 때
function a(x: string): number {
return 0;
}
type B = (x: string) => number | string;
let b: B = a;
반대인 상황 b c a 에서는 에러가 뜬다.
넓은 타입을 좁은 타입에 대입하려고 하면 문제가 생기는데 당연하다.
function a(x: string): number | number {
return 0;
}
type B = (x: string) => number;
let b: B = a;
이번에는 리턴값 타입은 같은데 매개변수 간의 타입이 다른 경우
매개변수를 보면 string c string | number인 상황이다. 결과는 오류뜬다.
function a(x: string): number {
return 0;
}
typeB = (x: string | number) => number;
let b: B = a; // Error - string | number 형식은 number 형식에 할당할 수 없다.
아래와 같은 로꾸꺼 형식 가능
function a(x: string | number): number {
return 0;
}
typeB = (x: string) => number;
let b: B = a;
조건부 타입으로 리턴값과 매개변수 타입 간의 속함을 비교하면 아래와 같이 정의된다.
// 조건부 타입: T가 P에 속해있으면 ? true : false
type IsSubtypeOf<T, P> = T extends P ? true : false;
function param1(x: string): number {
return +x;
}
function param2(x: string | number | boolean): number {
return +x;
}
function return1(x: string): number {
return +x;
}
function return2(x: string): number | string | boolean {
return +x;
}
type T1 = IsSubtypeOf<typeof param1, typeof param2>; // false
type T2 = IsSubtypeOf<typeof param2, typeof param1>; // true
type T3 = IsSubtypeOf<typeof return1, typeof return2>; // true
type T4 = IsSubtypeof<typeof return2, typeof return1>; // false
이변성
TypeScript는 기본적으로 함수의 인자를 다루는 과정에서 이변성 구조를 가지고 있다. 즉 공변성과 반공변성을 동시에 가지고 있어서 Casting 가능한 어떤 객체든 허용이 된다.
이러한 함수 인자가 이변성 이라는 오류를 바로잡아 반공변적이게 바꿔주는 옵션이 stringFunctionTypes이다.
tsconfig.json 파일로 가서 주석을 풀고 false로 설정한 다음 아래 코드를 치면 에러가 뜨지 않는다.
type A = (p: string) => void;
type B = (p: string | boolean | number[]) => void;
let a: A = (p: string) => {};
let b: B = (p: string | boolean | number[]) => {};
// 서로 매개변수 타입이 다름에 불구하고 막 대입이 가능하다.
b = a;
a = b;
--strictFunctionTypes 옵션을 비활성화 하면 매개변수 타입이 다른 함수끼리 막 대입이 가능한데 이게 이변성이다. 매개변수가 이변성을 가지는 데에는 이유가 있다. 리턴값은 공변성 가지고 매개변수는 반공변성 가짐으로 생기는 문제가 많기 때문이다.
현실적으로 수많은 @types/Definition 파일들에 의해 돌아가는 typescript의 특징 상, stringFunctionTypes를 활성화 시켰을 때 발생하는 에러들이 많아지는건 어쩔 수 없다. 외부 라이브러리를 다루는 과정에서 --strictFunctionTypes를 활성화 시키면 어쩔 수 없이 @ts-ignore를 넣어야 하는 경우가 생긴다.