Variance — 공변성, 반공변성, 이변성이 타입 호환에 미치는 영향
Variance(변성)는 타입 매개변수의 상속 관계가 제네릭 타입에서 어떻게 유지되는지 를 설명하는 개념입니다.
이 주제는 이론적이지만, 면접에서 "왜 함수 매개변수는 반공변적인가요?" 같은 질문이 나오기도 합니다.
기본 개념
Dog extends Animal일 때:
- 공변(Covariant):
Container<Dog>→Container<Animal>(같은 방향) - ** 반공변(Contravariant)**:
Container<Animal>→Container<Dog>(반대 방향) - ** 이변(Bivariant)**: 양쪽 다 가능
- ** 불변(Invariant)**: 양쪽 다 불가
class Animal { name = 'animal'; }
class Dog extends Animal { bark() { return '멍'; } }
class Cat extends Animal { meow() { return '야옹'; } }
공변성(Covariance)
** 출력 위치 **(반환 타입, readonly 속성)에서 나타납니다.
// 배열은 공변적
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // ✅ OK — Dog[]은 Animal[]에 할당 가능
// Dog extends Animal이면 Dog[] extends Animal[]
// 반환 타입은 공변적
type Producer<T> = () => T;
const produceDog: Producer<Dog> = () => new Dog();
const produceAnimal: Producer<Animal> = produceDog; // ✅ OK
직관적으로 "Dog을 반환하는 함수는 Animal을 반환하는 함수로도 쓸 수 있다"는 것이 자연스럽습니다.
반공변성(Contravariance)
** 입력 위치 **(함수 매개변수)에서 나타납니다.
// strictFunctionTypes: true일 때
type Consumer<T> = (value: T) => void;
const consumeAnimal: Consumer<Animal> = (animal) => {
console.log(animal.name);
};
const consumeDog: Consumer<Dog> = (dog) => {
dog.bark(); // Dog 전용 메서드 사용
};
// Animal을 받는 함수는 Dog을 받는 위치에서도 안전
const dogHandler: Consumer<Dog> = consumeAnimal; // ✅ OK
// Dog 전달 → Animal로 받아도 OK (Animal의 기능만 사용하므로)
// 반대는 안전하지 않음
// const animalHandler: Consumer<Animal> = consumeDog; // ❌ Error (strict 모드)
// Cat 전달 → Dog으로 받으면 bark()가 없어서 에러
이변성(Bivariance)
TypeScript는 strictFunctionTypes: false일 때 함수 매개변수가 ** 이변적 **입니다. 이는 JavaScript의 기존 패턴과의 호환을 위한 것이지만, 타입 안전하지 않습니다.
// strictFunctionTypes: false (레거시)
type Handler = (event: Event) => void;
const mouseHandler: Handler = (e: MouseEvent) => {
console.log(e.clientX); // ⚠️ MouseEvent가 Event 위치에 할당됨
};
// 양방향 모두 허용 — 안전하지 않음
strictFunctionTypes: true(권장)로 설정하면 메서드가 아닌 함수는 반공변적으로 검사됩니다.
메서드 시그니처 vs 함수 속성
interface Example {
// 메서드 시그니처 — 이변적 (strictFunctionTypes에서도)
method(value: Dog): void;
// 함수 속성 — 반공변적 (strictFunctionTypes에서)
fn: (value: Dog) => void;
}
공부하다 보니 메서드 시그니처가 이변적인 이유가 내장 타입(Array.push 등)의 호환성 때문이더라고요.
TS 4.7: 명시적 Variance 표기
// out = 공변 (출력 위치에서만 사용)
type Producer<out T> = () => T;
// in = 반공변 (입력 위치에서만 사용)
type Consumer<in T> = (value: T) => void;
// in out = 불변 (양쪽에서 사용)
type Processor<in out T> = (value: T) => T;
out과 in 표기는 ** 타입 체크 성능을 개선 **합니다. TypeScript가 variance를 계산하지 않고 바로 알 수 있기 때문입니다.
실전 영향
// ReadonlyArray는 공변적 (읽기만 하므로)
const dogs: readonly Dog[] = [new Dog()];
const animals: readonly Animal[] = dogs; // ✅ OK
// 일반 Array도 공변적으로 취급되지만...
const mutableDogs: Dog[] = [new Dog()];
const mutableAnimals: Animal[] = mutableDogs; // ✅ OK (TS에서는 허용)
mutableAnimals.push(new Cat()); // ⚠️ 런타임에서 문제! Cat이 Dog 배열에 들어감
TypeScript는 배열의 mutability를 엄격하게 검사하지 않습니다. 이는 편의를 위한 선택이지만 알고 있어야 합니다.
정리
- 공변성: 출력 위치에서 상속 방향이 유지된다 (Producer, 반환 타입)
- 반공변성: 입력 위치에서 상속 방향이 반전된다 (Consumer, 매개변수)
strictFunctionTypes: true를 켜면 함수 매개변수가 반공변적으로 검사된다out(공변),in(반공변) 표기로 variance를 명시하면 성능이 개선된다- 배열의 공변성은 TypeScript의 알려진 unsoundness 중 하나다
댓글 로딩 중...