BivarianceHack에 대해 알아보자

창건·2023년 5월 10일
7
post-thumbnail

곳곳에 숨겨진 BivarianceHack

Typescript + React로 작업을 하다보면, Third Party와 React 모듈에서 'BivarianceHack'이라는 것으로 타입이 정의된 것을 심심찮게 볼 수 있습니다.

BivarianceHack은 Bivariance를 사용할 수 있는 우회 통로입니다.
그렇다면 Bivariance가 뭘까요?

이를 이해하려면 먼저 Covariance와 Contravariance에 대해 이해해야 합니다.

공변성 (Covariance)

A가 B의 SubType일 때 T<A>가 T<B>의 Subtype인 경우를 의미합니다.

A -> B 이면 T<A> -> T<B>

말로 풀어내면 복잡하지만, 예시를 보면 간단합니다.

먼저 User 클래스와 이를 확장하는 자식 클래스 Admin을 만들어 보겠습니다.

즉, Admin은 User의 Subtype입니다.

class User {
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

class Admin extends User {
    isSuperAdmin: boolean;

    constructor(username: string; isSuperAdmin: boolean) {
        super(username);
        this.isSuperAdmin = isSuperAdmin;
    }
}

타입스크립트스럽게 이를 확인하기 위해서 IsSubtypeOf라는 헬퍼 타입을 만들어 보겠습니다.

type IsSubtypeOf<A, B> = A extends B ? true : false

type Test1 = IsSubtypeOf<Admin, User>;	// true
type Test2 = IsSubtypeOf<User, Admin>;	// false

그럼 다시 본론으로 돌아와서, 공변성이 무엇인지 다시 한번 보겠습니다.

A -> B 이면 T<A> -> T<B>

Promise를 통해 예시를 들어보겠습니다.

Promise은 Promise의 Subtype인가요?

다시 말해, 공변적인가요?

type Quiz1 = IsSubtypeOf<Promise<Admin>, Promise<User>>	// true

좀 더 쉬운 예시를 들어보겠습니다.

type Quiz3 = IsSubtypeOf<Array<string>, Array<string | number>> // true

Covariance 덕분에 다음과 같은 동작이 가능해 집니다.

let GeneralArray: Array<string | number>;
let stringArray: Array<string> = ['hello', 'variance'];
GeneralArray = stringArray;

반대로 대입하는 것은 불가능하겠죠?

let GeneralArray: Array<string>;
let stringArray: Array<string | number> = ['hello', 'variance'];
GeneralArray = stringArray;

반공변성 (ContraVariance)

반공변성은 말 그대로 공변성의 반대입니다.

A -> B 이면 T<B> -> T<A>

공변성은 직관적으로 다가오는데, 반공변성은 다소 생소하게 느껴집니다.

기본적으로 타입스크립트는 공변적이라고 볼 수 있습니다. 하지만 몇 가지가 예외적으로 반공변성을 가집니다.

함수가 그렇습니다.

Func라는 함수 타입을 정의해 보겠습니다.

type Func<ParamType> = (param: ParamType) => void;

Func는 공변적일까요 반공변적일까요?

type test1 = IsSubtypeOf<string, string | number>;	// true
type test2 = IsSubtypeOf<Func<string>, Func<string | number>>;	// ??

정답은 False, 반공변성을 가집니다.

반공변성은 공변성처럼 직관적이진 않은 것 같습니다.

좀 더 구체적인 예시를 들어보겠습니다.

let logNumber = function (param: number) {
    console.log(param);
}

let logSomething = function (param: number | string) {
    console.log(param);
}

logSomething = logNumber;	// Error
logNumber = logSomething;	// OK

만약 logSomething = logNumber가 성립한다면 logSomething의 인자로 들어오는 숫자와 문자열이 logNumber의 인자로 전달 됩니다.

하지만 logNumber는 number를 인자로 받고 있죠?

그렇기 때문에 함수는 인자에 대해서 반공변적인 것입니다.

이변성 (Bivariance)

이변성은 공변성과 반공변성을 모두 가지는 구조를 의미합니다.

A -> B이면 T<A> -> T<B>와 T<B> -> T<A>가 모두 성립한다.

반공변성의 함수 예시에서 logSomething = logNumber가 불가능한 이유는 반공변성 때문입니다.

이는 타입스크립트의 strictFunctionTypes옵션이 기본적으로 true로 설정되어 있기 때문입니다.

해당 옵션을 끄면, 함수는 인자에 대해서 이변성을 가지게 됩니다.

// "strictFunctionTypes": false

type IsSubtypeOf<A, B> = A extends B ? true : false;

let logNumber = function (param: number) {
    console.log(param);
}

let logSomething = function (param: number | string) {
    console.log(param);
}

logSomething = logNumber;	// OK
logNumber = logSomething;	// OK

이변성을 허용하면 생각보다 오류가 많이 생길 수 있습니다.

그렇기 때문에 strictFunctionTypes 옵션은 기본적으로 true로 설정되어 있습니다.

BivarianceHack

BivarianceHack은 strictFunctionTypes가 true일 때도 함수가 bivariance를 가질 수 있게 해줍니다.

이는 strictFunctionTypes가 함수에만 적용되고, 객체 메소드에는 적용되지 않는 것을 이용합니다.

Bivariance가 허용되면 오류가 생길 수도 있는데 왜 Hack이 존재하는 걸까요?

Typescript 공식문서의 예시를 보면서 알아 보겠습니다.

enum EventType {
    Mouse,
    Keyboard,
}

interface Event {
    timestamp: number;
}

interface MyMouseEvent extends Event {
    x: number;
    y: number;
}

interface MyKeyboardEvent extends Event {
    keycode: number;
}

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    //..
}

// strictFunctionTypes === true이면 오류!
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x, e.y)); // Error

// 어쩔수 없이 assertion을 사용해야 한다.
listenEvent(EventType.Mouse, (e: Event) => console.log((e as MyMouseEvent).x, (e as MyMouseEvent).y))

listenEvent가 좀 더 유연하게 동작할 수 있게 만들려면 어떻게 해야할까요?

handler를 함수가 아닌 메소드 타입으로 우회하면 가능합니다.

function listenEvent(eventType: EventType, handler: {
    bivarianceHack(e: Event): void;
}['bivarianceHack']) {
    //
}

//...

listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x, e.y)); // OK !
profile
피곤한만큼 성장할 수 있으면

0개의 댓글