
타입스크립트의 타입 시스템을 공부하면서 문득 다음과 같은 궁금증이 생겼다.
원시 타입, 객체 타입은 타입 호환성을 따지는 것은 어렵지 않다. 하지만 함수는 어떻게 타입 호환성이 결정될까?
interface Person {
name: string;
age: number;
}
function setPersonInfo(person: Person) { /* ... */ }
const person = { name: '철수', age: 25, address: 'seoul' };
setPersonInfo(person); // ✅ OK, because of structural typing
여기서 person 변수에 address 속성이 있음에도 setPersonInfo의 인자로 들어갈 수 있다. 이는 구조적 타이핑 시스템 관점에서 { name: '철수', age: 25, address: 'seoul' } 타입은 Person 타입에 있는 속성을 모두 가지고 있기 때문에 구조적으로 이 두 타입은 호환된다고 말할 수 있다.
그러면 함수 타입의 호환성은 어떻게 설명할 수 있는가? 왜 첫번째 할당문은 가능하고 두번째 할당문을 에러를 보여주는 걸까?
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // ✅ OK
x = y; // ❌ Error
type Supertype = { x: boolean }
type Subtype = { x: boolean, y: number }
let superTypedObj: Supertype;
let subTypedObj: Subtype = { x:true, y: 10 };
superTypedObj = subTypedObj; // ✅ OK, because of structural typing
Subtype이 Supertype의 구현체라고 명시적으로 해주지 않았기 때문이다.SubType인 { x: boolean, y: number }가 프로퍼티 x를 가지고 있어 구조적 타입 시스템에서는 SubType이 SuperType의 부분집합(서브타입)이 되는 것이다.
A는 B의 부분집합이라는 말은 A는 B의 서브타입, A는 B에 할당가능, A는 B를 상속과 모두 같은 의미이다.
하지만 아래처럼 객체 리터럴로 할당하는 경우 구조적 타입 시스템에서도 할당할 수 없다는 오류를 보여준다. 객체 리터럴을 할당하는 경우 그 타입에 있는 속성만 포함해서 할당해주도록 타입스크립트가 오류를 보여주는 것이다. 이를 잉여 속성 체크라고 부른다.
잉여 속성 체크를 통해 예상치 못한 오류를 방지해줄 수 있다.
typescript playground
interface Person {
name: string;
age: number;
}
function setPersonInfo(person: Person) { /* ... */ }
const person = { name: '철수', age: 25, address: 'seoul' };
setPersonInfo(person); // ✅ OK, because of structural typing
setPersonInfo({ name: '철수', age: 25, address: 'seoul' }); // 타입 에러 발생❗️
*덕 타이핑(duck typing)
덕 타이핑이란
동적 타이핑의 한 종류로 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다. 덕 테스트에서 유래되었다. '만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다'
글 처음에 쓴 예제를 다시 가져왔다.
typescript playground
// x's type - (a: number) => number
let x = (a: number) => 0;
// y's type - (b: number, s: string) => number
let y = (b: number, s: string) => 0;
y = x; // ✅ OK
x = y; // ❌ Error
y = x 에서 x의 파라미터가 y의 파라미터에 모두 있어(호환되어서) y에 x를 할당할 수 있다.x = y 에서 y는 두번째 파라미터(s: string)을 요구하므로 에러가 발생한다.y = x처럼 y가 파라미터 2개를 가지고 있음에도 x가 할당 가능함을 이미 이용하고 있었다.let items = [1, 2, 3];
// Don't force these extra parameters
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));
forEach 메서드는 콜백함수의 매개변수로 (value, index, array) 이 3개를 요구하지만 3개 모두 사용하지 않아도 된다. let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // OK
y = x; // Error, x()가 location 프로퍼티가 없기 때문
=)의 오른쪽 함수의 반환타입이 왼쪽 함수의 반환타입의 서브타입이어야 한다.y 함수의 반환타입이 { name: "Alice", location: "Seattle" }이고 x 함수의 반환타입이 { name: "Alice" }이므로 y의 반환타입이 x의 반환타입의 서브타입이다.Q. Vertex4D는 Vertex3D의 서브타입이고 Vertex3D는 Vertex3D의 서브타입일 때, 1️⃣, 2️⃣ 중 어느 것이 정상인가요? 그 이유는 무엇인가요?

interface Vertex2D {
x: number;
y: number;
}
interface Vertex3D {
x: number;
y: number;
z: number;
}
interface Vertex4D {
x: number;
y: number;
z: number;
w: number;
}
type Fn1 = (a: number, b: number, c: number, d: number) => Vertex2D
type Fn2 = (a: number, b: number, c: number) => Vertex3D
let foo: Fn1 = (arg) => {
const vertex: Vertex2D = { x: 1, y: 2 };
return vertex;
}
let bar: Fn2 = (arg) => {
const vertex: Vertex3D = { x: 1, y: 2, z: 3 };
return vertex;
}
foo = bar; // 1️⃣
bar = foo; // 2️⃣
1️⃣이 정상이다. 그 이유를 알기 위해선 foo와 bar의 함수 타입을 비교해야하고, 파라미터 비교와 반환타입 비교를 해보면 된다.
파라미터 비교
foo의 파라미터는 (number, number, number, number)이고 bar의 파라미터는 (number, number, number)이다. 즉, bar의 모든 파라미터가 foo의 파라미터와 호환가능하다(그 반대는 안됨)
반환 타입 비교
foo의 반환 타입은 Vertex2D, bar의 반환 타입은 Vertex3D이다. Vertex3D가 Vertex2D의 서브타입이므로 foo = bar 가 가능하다.(그 반대는 안됨)
따라서 1️⃣이 정상이다