해당 블로그 글은 이 글을 바탕으로 내용을 정리한 내용입니다.
타입스크립트에 대해 공부하다, 같이 공부하던 분이 추천해 준 글(위의 링크)을 통해 객체 메서드를 정의하는 기준을 타입관점에서 생각해 볼 수 있게 되었다.
흔히 객체의 메서드를 정의하는 방법은 화살표 함수
를 이용하거나 속성 단축법
을 이용한다는 사실은 자바스크립트를 공부하 사람 모두 다 알고 있다.
const object = {
add : (a, b) => return a + b,
subtract (a, b) => return a - b,
}
하지만 두 방법에는 타입 관점에서 차이점이 존재한다. 차이점을 알기 위해서, 우리는 위 글에서 소개한 공변
과 반변
에 대해 알아야 한다.
말이 어렵다. 나는 위의 개념을 집합 관점에서 이해했다.
공변은 상위 타입
과 서브 타입
의 관계가 고차 타입으로 변환되어도 유지되는 것을 말한다.
- 서브 타입이란 상위 타입을 상속받은 타입을 말한다.
- 고차 타입이란 고차 함수, 고차 컴포넌트와 같이 타입(input)을 받아 새로운 타입(output)을 반환하는 타입을 말한다.
위 글에서 소개한 Animal 과 Dog로 살펴보면,
interface Animal {
id : string;
}
interface Dog extends Animal {
name : string;
}
Animal은 Dog 보다 더 넓은 타입이다. 더 넓다는 건 제약 조건
이 적다는 것이다. Animal이 되기 위해서는 id
프로퍼티만 있어도 되지만, Dog 타입은 추가로 name
프로퍼티가 필요하기 때문이다.
따라서 Dog이면 Animal(Dog → Animal)은 맞지만, Animal이라고 해서 Dog(Animal → Dog)인 건 아니다. 타입이 더 넓다는 건, 더 넓은 타입에 좁은 타입을 할당할 수 있다는 걸 말한다.
공변이란 이런 집합 관계가 고차 타입으로 변환되어도 유지되는 것을 말한다.
let animalArray : Array<Animal>;
let dogArray : Array<Dog>;
animalArray = dogArray;
// Dog 타입의 배열은 결국 Animal 타입의 배열과도 같다.
// 더 넓은 타입의 변수에 더 좁은 타입의 값을 할당할 수 있다.
다음과 같은 타입을 공변이라고 한다.
type Return<T> = () => T
반변은 이런 집합 관계가 역으로 성립하는 걸을 말한다.
즉, 고차 타입으로 변환된 타입의 관계가 기존 상위 타입과 서브 타입의 관계에서 역전된다는 것이다.
type View<T> = (value : T) => void; // 반변
let viewAnimal : View<Animal> = (value) => console.log(value.id);
let viewDog : View<Dog> = (value) => console.log(value.id, value.name)
viewDog = viewAnimal // 가능
viewAnimal = viewDog // 불가능
이렇게 제공한 타입(T)을 함수의 매개변수로 사용하는 함수 타입은 반변한다.
왜 해당 타입이 반변하는지 잠시 생각해보면, 오히려 이렇게 하는 것이 타입의 엄격함을 유지할 수 있기 때문이다.
함수에 전달한 매개변수의 속성은 해당 함수 내에서 모두 접근 가능해야 한다. 즉 내가 Dog 타입을 매개변수로 전달했다는 건, 해당 함수 내에서 Dog 타입의 속성을 모두 사용할 수 있을 거라 기대하는 것이다.
하지만 Animal 타입의 매개변수를 전달하게 되면, Animal에는 없는 Dog의 name 속성을 사용할 수 없다. 따라서 이 때는 더 넓은 타입이 아닌 더 좁은 타입으로 한정하는 것이 더 안전하다.
조금만 더 확장하면, Dog 보다 더 좁은 타입도 해당 매개변수로 올 수 있다.
interface Maltese extends Dog {
color : string;
}
let viewMaltes : View<Maltese> = (value) => console.log(value.id, value.name, value.color);
viewMaltes = viewDog;
viewDog는 매개변수로 id와 name 속성을 가진 객체값이 필요하다. 할당 받는 타입(viewMaltes
)은 매개변수로 id, name 뿐만 아니라 color 속성까지 포함된 타입의 매개변수를 요구한다. 따라서 viewMaltes
의 매개변수로 사용된 값의 타입은 viewDog가 원하는 매개변수의 속성을 모두 갖고 있음이 보장된다.
객체의 메서드를 정의할 때, 화살표 함수로 정의할 경우 반변과 공변의 원칙에 따라 더 안전한 타입 가드가 가능하지만 단축 속성으로 정의할 경우 실제로 양변 타입이 되어 타입의 안정성을 보장할 수 없다.
// (1)
type View<T> = (v: T) => void // 반변
type WithView<T> = { view: T } // 공변
type Viewer<T> = WithView<View<T>> // 반변 (반변 && 공변 => 반변)
----
// (2)
type View<T> = () => T // 공변
type WithView<T> = { view: T } // 공변
type Viewer<T> = WithView<View<T>> // 공변 (공변 && 공변 => 공변)
// (1) 과 같이 반변
interface Viewer<T> {
view: (v: T) => void
}
declare let animalViewer: Viewer<Animal>
declare let dogViewer: Viewer<Dog>
dogViewer = animalViewer // OK!
animalViewer = dogViewer // Error!, 단 단축 표기법으로 정의할 경우 양변 타입이 되어 에러가 발생하지 않는다.
따라서 메서드를 정의할 경우 화살표 함수로 정의하는 것이 타입 관점에서 더 좋다는 사실을 알 수 있다.