이 글은 ChatGPT에 질의한 내용을 바탕으로 구성하였습니다.
TypeScript에서 함수의 인자 타입에는 반공변성(contravariance)이 적용되고, 반환값 타입에는 공변성(covariance)이 적용되는 이유를 직관적으로 설명하면 다음과 같습니다:
함수의 인자는 함수 호출자가 제공하는 값입니다. 호출자는 제공한 값이 함수 내부에서 안전하게 처리될 수 있다고 기대해야 합니다. 따라서, 함수가 받는 인자의 타입은 더 일반적(generic)일수록 안전합니다.
type Animal = { name: string };
type Dog = Animal & { bark: () => void };
function handleAnimal(animal: Animal) {
console.log(animal.name);
}
const dogHandler: (dog: Dog) => void = handleAnimal; // 안전
handleAnimal
은 모든 Animal
을 처리할 수 있으므로 Dog
도 처리 가능.
반대로, handleAnimal
을 Dog
에 특화된 함수로 대체하면, Animal
을 처리할 때 문제가 생길 수 있습니다.
직관적으로: 함수의 인자는 함수 내부에서 소비되는 데이터이므로, 더 구체적인 타입(제한적인 타입)을 요구하면 호출자의 자유를 침해합니다.
함수의 반환값은 함수 호출자가 사용하는 값입니다. 호출자는 반환값이 자신이 예상한 타입(혹은 그보다 더 구체적인 타입)이라고 기대합니다. 따라서, 함수가 반환하는 값의 타입은 더 구체적(specific)일수록 안전합니다.
type Animal = { name: string };
type Dog = Animal & { bark: () => void };
function getDog(): Dog {
return { name: "Buddy", bark: () => console.log("Woof!") };
}
const animalGetter: () => Animal = getDog; // 안전
getDog
은 Dog
객체를 반환하지만, Animal
을 기대하는 함수 타입으로 대체 가능합니다.
반대로, Animal
만 반환하는 함수를 Dog
를 기대하는 타입으로 대체하면, 호출자가 bark()
메서드에 접근할 때 문제가 생깁니다.
직관적으로: 함수의 반환값은 함수 외부에서 소비되는 데이터이므로, 더 일반적인 타입(포괄적인 타입)을 반환하면 호출자의 기대를 만족하지 못합니다.
요약 직관:
그래서: 인자는 반공변적, 반환값은 공변적.
반공변성의 예시에서 바로 이해가 잘 되지 않는데, 일반적인 변수 할당을 보면,
const animal: Animal = dog;
이렇게 좌변의 변수 관점에서 생각하잖아. 그런데 인자의 경우는,
const dogHandler: (dog: Dog) => void = (animal: Animal) => void;
이렇게 좌변의 변수 관점이 아니라 우변의 인자 관점에서 생각하네. 이러한 차이를 직관적으로 설명할 수 있을까?
좋은 질문이에요! 함수 타입의 인자에서 왜 우변의 인자 타입(함수 정의 내부에서의 관점)으로 생각해야 하는지, 이를 일반 변수 할당과 비교하여 직관적으로 설명하겠습니다.
const animal: Animal = dog;
animal
변수는 Animal
타입이라고 선언했으니, 더 구체적인 타입인 Dog
도 할당 가능. 이것은 공변성에 기반한 동작입니다.dog
는 Animal
타입을 만족하므로 안전합니다.이 경우, 좌변은 기대치를 나타내고, 우변은 그 기대치를 충족시키는가?를 확인하는 구조입니다.
const dogHandler: (dog: Dog) => void = (animal: Animal) => void {};
여기서 혼란스러운 점은, 인자 타입이 우변의 함수 정의에서 결정된다는 것입니다. 왜냐하면, 함수의 인자는 함수 호출자가 제공하는 값에 의해 사용되기 때문입니다.
좌변의 함수 타입:
dogHandler
는 (dog: Dog) => void
를 기대합니다.
즉, 이 함수는 반드시 Dog를 인자로 받는 함수여야 합니다.
우변의 함수 구현:
우변에서 (animal: Animal) => void
는 Animal
을 인자로 받아도 문제없이 동작하는 함수입니다.
결과적으로, 이 함수는 Dog
를 처리하는 데 충분히 안전합니다.
함수 타입의 본질은 함수 호출자와 함수 구현 간의 계약입니다:
Animal
도 처리할 수 있으니, Dog
역시 문제없다."따라서 우변의 (animal: Animal)
은 호출자가 Dog
를 넘겼을 때도 안전하게 처리할 수 있는 구조입니다.
일반 변수:
const animal: Animal = dog;
Animal
)를 수용할 준비가 되어 있다.함수 타입:
const dogHandler: (dog: Dog) => void = (animal: Animal) => void {};
이 비유에서 핵심은 누가 데이터를 제공하고, 누가 데이터를 소비하느냐입니다:
함수 인자의 타입이 반공변적인 이유는, 함수 호출자가 제공하는 데이터에 맞춰 함수가 더 일반적인 경우에도 안전하게 작동해야 하기 때문입니다.
우변에서 정의한 함수가 "더 많은 상황을 처리할 수 있다"고 생각하면 직관적 이해가 쉬워질 거예요.
비슷한 논리 구조로 함수 반환 타입이 공변적인 이유를 설명해 줘.
좋아요! 함수 반환 타입이 공변적(covariant)인 이유를, 방금 설명한 논리 구조에 맞춰 직관적으로 설명하겠습니다.
함수의 반환값은 함수 호출자(사용자)가 소비하는 데이터입니다.
즉, 호출자는 반환값이 내가 기대한 타입이거나, 그보다 더 구체적인 타입이길 기대합니다.
좌변: 기대치
const animalGetter: () => Animal = getDog;
animalGetter
는 Animal
을 반환하는 함수를 기대하고 있습니다.
좌변은 "나는 최소한 Animal
을 반환하는 함수면 된다"는 기대치를 나타냅니다.
우변: 구현
function getDog(): Dog {
return { name: "Buddy", bark: () => console.log("Woof!") };
}
우변의 getDog
는 Dog
를 반환합니다.
Dog
는 Animal
을 포함하는 더 구체적인 타입이므로, 좌변의 기대치를 충족할 수 있습니다.
호출자 입장에서 함수의 반환값은 "내가 쓸 데이터"입니다.
데이터는 더 구체적일수록 호출자가 예상한 동작을 더 많이 제공하기 때문에 안전합니다.
예시:
type Animal = { name: string };
type Dog = Animal & { bark: () => void };
// 기대: Animal 반환
const animalGetter: () => Animal = () => ({ name: "Buddy" });
// 더 구체적인 반환값을 가진 함수 할당: 안전
const dogGetter: () => Dog = () => ({ name: "Buddy", bark: () => console.log("Woof!") });
animalGetter = dogGetter; // OK
좌변의 animalGetter
는 최소한 name: string
을 가진 객체를 반환하면 됩니다.
우변의 dogGetter
는 추가로 bark()
메서드를 가진 더 구체적인 객체를 반환합니다.
호출자는 좌변에 선언된 타입만 기대하므로, dogGetter
가 반환하는 값은 문제없이 사용 가능합니다.
반대 경우 (안전하지 않음):
const dogGetter: () => Dog = () => ({ name: "Buddy", bark: () => console.log("Woof!") });
const animalGetter: () => Animal = () => ({ name: "Buddy" });
dogGetter = animalGetter; // Error
좌변의 dogGetter
는 반드시 bark()
메서드를 가진 객체를 반환해야 합니다.
우변의 animalGetter
는 bark()
메서드가 없는 객체를 반환하므로, 호출자가 bark()
를 기대하면 문제가 발생합니다.
함수 반환 타입:
일반 데이터 할당과의 차이:
함수 반환값은 호출자가 소비하는 데이터이므로, 반환 타입은 더 구체적일수록 안전합니다.
호출자가 기대하는 최소한의 조건(Animal
)을 충족하고, 추가적인 정보를 더 포함할 수 있습니다.
따라서 함수 반환 타입에는 공변성(covariance)이 적용됩니다.