Variance(변성)는 타입 매개변수의 상속 관계가 제네릭 타입에서 어떻게 유지되는지 를 설명하는 개념입니다.

이 주제는 이론적이지만, 면접에서 "왜 함수 매개변수는 반공변적인가요?" 같은 질문이 나오기도 합니다.

기본 개념

Dog extends Animal일 때:

  • 공변(Covariant): Container<Dog>Container<Animal> (같은 방향)
  • ** 반공변(Contravariant)**: Container<Animal>Container<Dog> (반대 방향)
  • ** 이변(Bivariant)**: 양쪽 다 가능
  • ** 불변(Invariant)**: 양쪽 다 불가
TYPESCRIPT
class Animal { name = 'animal'; }
class Dog extends Animal { bark() { return '멍'; } }
class Cat extends Animal { meow() { return '야옹'; } }

공변성(Covariance)

** 출력 위치 **(반환 타입, readonly 속성)에서 나타납니다.

TYPESCRIPT
// 배열은 공변적
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)

** 입력 위치 **(함수 매개변수)에서 나타납니다.

TYPESCRIPT
// 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의 기존 패턴과의 호환을 위한 것이지만, 타입 안전하지 않습니다.

TYPESCRIPT
// strictFunctionTypes: false (레거시)
type Handler = (event: Event) => void;
const mouseHandler: Handler = (e: MouseEvent) => {
  console.log(e.clientX); // ⚠️ MouseEvent가 Event 위치에 할당됨
};
// 양방향 모두 허용 — 안전하지 않음

strictFunctionTypes: true(권장)로 설정하면 메서드가 아닌 함수는 반공변적으로 검사됩니다.

메서드 시그니처 vs 함수 속성

TYPESCRIPT
interface Example {
  // 메서드 시그니처 — 이변적 (strictFunctionTypes에서도)
  method(value: Dog): void;

  // 함수 속성 — 반공변적 (strictFunctionTypes에서)
  fn: (value: Dog) => void;
}

공부하다 보니 메서드 시그니처가 이변적인 이유가 내장 타입(Array.push 등)의 호환성 때문이더라고요.

TS 4.7: 명시적 Variance 표기

TYPESCRIPT
// out = 공변 (출력 위치에서만 사용)
type Producer<out T> = () => T;

// in = 반공변 (입력 위치에서만 사용)
type Consumer<in T> = (value: T) => void;

// in out = 불변 (양쪽에서 사용)
type Processor<in out T> = (value: T) => T;

outin 표기는 ** 타입 체크 성능을 개선 **합니다. TypeScript가 variance를 계산하지 않고 바로 알 수 있기 때문입니다.

실전 영향

TYPESCRIPT
// 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 중 하나다
댓글 로딩 중...