[우아한타입스크립트] 공변성과 반공변성 완전 이해하기

yesme·2024년 1월 2일
1
post-thumbnail

책 내용 살펴보기

📒 책 279p 내용 중...

타입 A가 B의 서브타입일 때, T<A>T<B> 의 서브타입이 된다면 공변성을 띠고 있다고 말한다.

하지만 제네릭 타입을 지닌 함수는 반공변성을 가진다. 즉, T<B>T<A>의 서브타입이 되어, 좁은 타입 T<A>의 함수를 넓은 타입 T<B>의 함수에 적용할 수 없다는 것을 의미한다.

네?

뭐라는거야천천히 다시 읽어보자...

A는 위 예시에서 User, B는 Member를 의미한다. 따라서 위 구절을 다시 말하자면

T<Member>T<User>의 서브타입이 되어, 좁은 타입 T<User>의 함수를 넓은 타입 T<Member>의 함수에 적용할 수 없다는 것을 의미한다.

이름에서 알수 있듯, 반공변성은 공변성의 반대로 동작한다.

공변성과 반공변성이란

예시와 함께 설명을 보자.

  • 공변성: 함수 리턴값, 일반 변수 등의 타입. 좁은 타입으로 선언한 변수에 넓은 타입으로 선언한 변수 할당 가능
  • 반공변성: 함수의 매개변수, 제네릭으로 선언된 함수. 넓은 타입으로 선언한 변수에 좁은 타입 변수 할당 가능

쉽게 설명해서,

단어 자체가 생소하고 갑자기 머리 아픈 코드들이 나와서 좀 헷갈리는데, 아래 예시를보면 정말 쉽다!

const a: {name: string} = {name: "yesme"};

라고 하는 변수에, {name: string, age: number} 타입을 가진 변수가 들어올 수 있을까?

.

.

.

가능하다!

const a: {name: string} = {name: "yesme", age: 365};

age가 추가되었을 뿐 name이라는 인자가 있으니 문제될 것이 없다. 하지만 반대라면?

불가능하다! age의 값이 없기 때문이다.

이것을 “공변성” 이라고 한다.


반대로 생각해보자.

type NameFun = (value: {name: string}) => unknown;

인자로 {name}을 받아, 계산 후 무언가를 반환하는 함수들에 대한 NameFun 타입을 정의했다.

그런데 만약,

function userInfo(value: {name: string; age: number}){
	return value;
}

name과 age의 인자를 받는 함수가 있다면?

이 함수는 NameFun 타입으로 정의할 수 있는가?

.

.

.

불가능하다!

코드로 보면 아래와 같다.

const nameInfo: NameFun = userInfo; // ERROR!

내가 선언한 함수(nameInfo - 좁은 타입) 인자에 필요한 age 값이 userInfo(넓은 타입)에는 없으므로 선언이 불가능하다.

타입보다 적은 범위의 인자를 받으면 정의된 함수는 누락된 타입을 보장할 수 없습니다. ... 즉, 넓은 타입을 정의한 a보다 타입 세이프(type safe)하지 못합니다.

이를 “반공변성” 이라고 한다.

문장으로 보면 복잡한데, 막상 풀어놓고 보면 간단하다!


+) 이변성

하나의 개념만 더 잡고가자.

  • 이변성: A -> B 일 때 T<A> -> T<B>도 되고 T<B> -> T<A>도 되는 경우

공변성 특징과, 반공변성 특징을 다 포함한다.

위 코드에서 Logger<string | number>의 서브타입이

  1. string | number | boolean → 전부 포함 +
  2. number → 일부만 포함

둘다 가능하다는 것이다.

(이는 "strictFunctionTypes": true 를 통해 메서드를 반공변성으로 만들어 줌으로써 제거 가능하다.)

타입스크립트에서의 활용

이제 책에 나왔던, 이해 안되던 예시를 다시 확인해보자. (정확한 이해라 어려워, 코드를 조금 변경했다)

onChangeA 같이 함수 타입을 화살표 표기법으로 작성한다면 반공변성을 띠게 된다.

onChangeA처럼 화살표 함수로 타입을 선언하게 되면 반공변성을 띠게 된다.
따라서 “apple” | “banana”를 포함해야만 하는 특성을 가진다.

또한 onChangeB와 같이 함수 타입을 지정하면 공변성과 반공변성을 모두 가지는 이변성을 띠게 된다.

이와 달리 onChangeB 처럼 선언해주는 함수는 이변성을 띤다. 따라서 “apple” 하나만 포함하거나,
"apple" | "banana" | "grape” 를 모두 포함하는 경우도 선언이 가능해진다.

정확한 타입추론을 위해, onChangeB 같은 방법은 지양하는 편이 좋을 것 같다.

결론

공변성, 반공변성, 이변성. 그 예시에 대해 살펴봤다.
겨우 책에 한 장 나오는(...) 아주 소소한 부분이지만 이해가 안되어서 찾아보다보니 내용이 조금 길어지게 된 것 같다.

그래도 정리하다보니 무슨 차이인지 확실히 이해할 수 있었던 것 같고
타입스크립트는 머리가 아닌 가슴으로 공부하는게 좋을 것 같다..^^!

profile
코드 깎는 개발자..

1개의 댓글

comment-user-thumbnail
2025년 1월 6일

안녕하세요! 글 잘 읽었습니다 ㅎㅎ
한가지 궁금한점이 있는데요!

맨 위에서 "일반 타입은 좁은 타입에 넓은 타입이 할당이 가능하다 -> 공변성" 이라고 말씀해주셨습니다.

한가지 예시를 들어보겠습니다.

let stringArray: Array = [];
let array: Array<string | number> = [];

array = stringArray; // OK
stringArray = array; // ERROR

여기서 좁은 타입은 Array이고, 넓은 타입은 Array<string | number> 입니다.
해당 예제에서는 좁은 타입(stringArray)에 넓은 타입(array)가 할당이 불가능한데요!

혹시 제가 이해를 잘 못하고 있는거라면 한번 개념을 짚어주실 수 있으실지 궁금합니다!
감사합니다.

답글 달기

관련 채용 정보