TypeScript 에서의 공변성과 반공변성 (strictFunctionTypes)

strictFunctionTypes 에서 등장하는 공변성 (Convariance), 반공변성 (Contravariance) 말이 어려워서 혼돈이 있을수 있는데, 한 마디로 정리하자면 일반적인 Sub Type 관계가 함수 인자에서는 반대로 적용된다는 의미가 된다.

type A = { a : string }
type B = { a : string, b : string }
// B <: AA = B // OK
B = A // Error

일반적인 공변적 Sub Type 관계는 위와 같다. B는 A를 포함하고 있기 때문에 B는 A의 Sub Type이 된다. B <: A

하지만, 함수의 인자에서는 이것이 반대로 적용되게 된다

type FA = (p : {a : string}) => void
type FB = (p : {a : string, b : string}) => void
// FA <: FBFA = FB // Error
FB = FA // OK

논리적으로 생각해보면 아주 당연한데…

  1. {a} 라는 인자를 처리하는 FA 에는 {a, b} 를 처리하는 FB 를 대입할 수 없다.
  2. {a, b} 라는 인자를 처리하는 FB 에는 {a} 를 처리하는 FA 가 대입될 수 있다.

이걸 풀어서 보자면 아래와 같다.

const a: FA = ({a, b}) => {
console.log(a, b)
// Error - FA({a})만 입력될 수 있기 때문에 b는 올 수 없다
}
const b: FB = ({a}) => {
console.log(a)
// OK - FB({a, b})가 오더라도, a만 사용하기 때문에 문제 없다
}

이런 상황들을 다시 정리해보자면 이렇게 된다.

type A = { a : string }
type B = { a : string, b : string }
// B <: A
// () => B <: () => A
// A => void <: B => void

테스트를 만들어 확인해 보자면 이렇게 된다

describe('strictFunctionTypes', () => {
test('test strict function types rules', () => {
type A = {a: string};
type B = {a: string, b: string};
type C = {a: string, b: string, c: string};

type FA = (p: A) => void;
type FB = (p: B) => void;
type FC = (p: C) => void;

const a: A = {a: 'a'};
const b: B = {a: 'a', b: 'b'};
const c: C = {a: 'a', b: 'b', c: 'c'};

const fa: FA = p => {
console.log(p.a);
};
const fb: FB = p => {
console.log(p.a, p.b);
};
const fc: FC = p => {
console.log(p.a, p.b, p.c);
};

// C <: B <: A
// (A => void) <: (B => void) <: (C => void)

// 공변 (Convariance)
const a1: A = a;
const a2: A = b;
const a3: A = c;
const b1: B = a; // Error - {a}는 {a, b}가 될 수 없다
const b2: B = b;
const b3: B = c;
const c1: C = a; // Error - {a}는 {a, b, c}가 될 수 없다
const c2: C = b; // Error - {a, b}는 {a, b, c}가 될 수 없다
const c3: C = c;

fa(a);
fa(b);
fa(c);
fb(a); // Error - param {a}는 {a, b}가 될 수 없다
fb(b);
fb(c);
fc(a); // Error - param {a}는 {a, b, c}가 될 수 없다
fc(b); // Error - param {a, b}는 {a, b, c}가 될 수 없다
fc(c);

// 반공변 (Contravariance) - strictFunctionTypes
const fa1: FA = fa;
const fa2: FA = fb; // Error - {a}만 처리하는 FA에 {a, b}를 처리하는 FB를 대입할 수 없다
const fa3: FA = fc; // Error - {a}만 처리하는 FA에 {a, b, c}를 처리하는 FC를 대입할 수 없다
const fb1: FB = fa;
const fb2: FB = fb;
const fb3: FB = fc; // Error - {a, b}만 처리하는 FB에 {a, b, c}를 처리하는 FC를 대입할 수 없다
const fc1: FC = fa;
const fc2: FC = fb;
const fc3: FC = fc;
});

test('test strict function types return type', () => {
type A = {a: string};
type B = {a: string, b: string};
type C = {a: string, b: string, c: string};

type FA = () => A;
type FB = () => B;
type FC = () => C;

const a: A = {a: 'a'};
const b: B = {a: 'a', b: 'b'};
const c: C = {a: 'a', b: 'b', c: 'c'};

const fa: FA = () => a;
const fb: FB = () => b;
const fc: FC = () => c;

const fa1: FA = fa;
const fa2: FA = fb;
const fa3: FA = fc;
const fb1: FB = fa; // Error - return {a}는 {a, b}가 될 수 없다
const fb2: FB = fb;
const fb3: FB = fc;
const fc1: FC = fa; // Error - return {a}는 {a, b, c}가 될 수 없다
const fc2: FC = fb; // Error - return {a, b}는 {a, b, c}가 될 수 없다
const fc3: FC = fc;
});
});

Generic

이런 공변성, 반공변성은 Generic을 다루는 과정에서 실수가 일어날 수 있다.

일반적으로 우리가 사용하는 type F<T> = (p: T) => T 형태의 Generic Function이 문제가 된다.

  • B <: A 의 관계가 있을때
  • [Error] B => B <: A => A 가 될 수 없기 때문이다. B => void <: A => void 가 될 수 없는 것처럼 인자는 반공변적이다.
  • [Error] 그렇기에 F<B> <: F<A> 는 성립되지 않는다
test('test strict function types generic function', () => {
type A = {a: string};
type B = {a: string, b: string};
type C = {a: string, b: string, c: string};

type F<T> = (p: T) => T;

const a: A = {a: 'a'};
const b: B = {a: 'a', b: 'b'};
const c: C = {a: 'a', b: 'b', c: 'c'};

const fa: F<A> = (p: A) => a;
const fb: F<B> = (p: B) => b;
const fc: F<C> = (p: C) => c;

const fa1: F<A> = fa;
const fa2: F<A> = fb; // Error - {a}만 처리하는 F<A>에 {a, b}를 처리하는 F<B>를 대입할 수 없다
const fa3: F<A> = fc; // Error - {a}만 처리하는 F<A>에 {a, b, c}를 처리하는 F<C>를 대입할 수 없다
const fb1: F<B> = fa; // Error - return {a}는 {a, b}가 될 수 없다
const fb2: F<B> = fb;
const fb3: F<B> = fc; // Error - {a, b}만 처리하는 F<B>에 {a, b, c}를 처리하는 F<C>를 대입할 수 없다
const fc1: F<C> = fa; // Error - return {a}는 {a, b, c}가 될 수 없다
const fc2: F<C> = fb; // Error - return {a, b}는 {a, b, c}가 될 수 없다
const fc3: F<C> = fc;
});

type F<T> = (p: T) => T 는 인자(Parameter)는 반공변성 흐름을 가지고, 반환(Return)은 공변성 흐름을 가지기 때문에 F<A>F<B> 간에 어떤 형태의 Type Cast 도 일어날 수 없다. T => T 는 무공변성(Invariance) 일 수 밖에 없다.

Type Cast 까지 고려를 한다면 반공변성 문제로 인해서 type F<P, R> = (p: P) => R 이 되어야만 한다.

test('test strict function types generic function', () => {
type A = {a: string};
type B = {a: string, b: string};
type C = {a: string, b: string, c: string};

type F<P, R> = (p: P) => R;

const a: A = {a: 'a'};
const b: B = {a: 'a', b: 'b'};
const c: C = {a: 'a', b: 'b', c: 'c'};

const faa: F<A, A> = (p: A) => a;
const fab: F<A, B> = (p: A) => b;
const fac: F<A, C> = (p: A) => c;
const fba: F<B, A> = (p: B) => a;
const fbb: F<B, B> = (p: B) => b;
const fbc: F<B, C> = (p: B) => c;
const fca: F<C, A> = (p: C) => a;
const fcb: F<C, B> = (p: C) => b;
const fcc: F<C, C> = (p: C) => c;

const fa1: F<C, A> = faa;
const fa2: F<C, A> = fab;
const fa3: F<C, A> = fac;
const fb1: F<C, A> = fba;
const fb2: F<C, A> = fbb;
const fb3: F<C, A> = fbc;
const fc1: F<C, A> = fca;
const fc2: F<C, A> = fcb;
const fc3: F<C, A> = fcc;

const list: F<C, A>[] = [
faa,
fab,
fac,
fba,
fbb,
fbc,
fca,
fcb,
fcc,
];
});

P => R 의 경우에서 어떤 경우던 수용하는 케이스는 함수의 인자가 가지는 반공변적 흐름의 최대치인 C 와 반환의 공변적 흐름의 최소치인 A 가 합쳐진 F<C, A> 가 된다.

TypeScript에서 strictFunctionTypes를 사용할때 어쩔 수 없는 부분들

TypeScript는 기본적으로는 함수의 인자를 다루는 과정에서 이변성(Bivariance) 구조를 가지고 있다. 즉, 공변성과 반공변성을 동시에 가지고 있고, 그로 인해서 Casting 가능한 어떤 객체든 허용한다는 문제를 가지고 있다.

이런 함수 인자가 이변성 이라는 오류를 바로잡아서 반공변적이게 바꿔주는 옵션이 strictFunctionTypes 이다.

그리고, strictFunctionTypes 는 반공변적이지 않은 함수 인자가 들어가는 것을 방지해주기 때문에 개발에 도움이 된다.

다만, 현실적으로 수많은 @types/ Definition 파일들에 의해 돌아가는 TypeScript의 특징 상, strictFunctionTypes 를 활성화 시켰을때 발생하는 불가항력적인 에러들이 많아지는건 어쩔 수 없다.

외부 라이브러리를 다루는 과정에서 strictFunctionTypes 를 활성화 시킨다면 별수 없이 @ts-ignore 를 넣어야 하는 경우가 꽤 있다.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store