타입스크립트를 사용하다 보면 아래 같은 상황을 자주 마주한다. Object.keys()
메서드를 사용해서 객체의 키를 배열로 추출한 후, 해당 키를 이용해 객체에 접근할 때 에러가 발생한다.
type Options = { host: string; port: number };
const validateOptions = (options: Options) => {
const keys = Object.keys(options); // string[]
keys.forEach(key => {
// Error! 'Options' 형식에서 'string' 형식의 매개 변수가 포함된 인덱스 시그니처를 찾을 수 없습니다.
if (options[key] === null) {
throw new Error(`Missing option ${key}`);
}
});
};
위 문제는 아래처럼 as
키워드를 사용해서 타입 단언하면 쉽게 해결할 수 있다. 이러한 문제는 왜 발생하는걸까?
const validateOptions = (options: Options) => {
const keys = Object.keys(options) as (keyof Options)[];
// ...
};
Object.keys
타입 정의를 살펴보면 객체를 인자로 받아 항상 string[]
을 반환하도록 되어 있다.
interface ObjectConstructor {
// ...
keys(o: object): string[];
}
Object.keys()
메서드가 객체 제네릭 타입 T
를 받아 (keyof T)[]
를 반환한다면 이러한 문제는 발생하지 않는다. 타입스크립트가 이렇게 타입을 정의하지 않은 이유는 무엇일까? 이는 구조적 타입 시스템과 관련 있다.
export {}; // declare global을 사용하기 위해 외부 모듈로 인식하도록 만듦
declare global {
interface ObjectConstructor {
// ObjectConstructor 인터페이스를 확장하여 Object.keys 메서드 타입 덮어쓰기
keys<T extends object>(o: T): (keyof T)[];
}
}
const validateOptions = (options: Options) => {
const keys = Object.keys(options) // (keyof Options)[]
// ...
};
<aside> <img src="/icons/search_gray.svg" alt="/icons/search_gray.svg" width="40px" /> 타입스크립트 컴파일러는 export 혹은 import 구문이 없는 파일을 일반 스크립트로 취급한다. declare global을 이용해 전역 참조할 수 있는 선언 코드를 작성하려면 export {} 등을 사용해 외부 모듈로 인식하도록 해야 한다 (참고 노트)
</aside>
구조적 타이핑(Structural Typing)은 코드 구조의 관점에서 타입 호환성을 판단하는 방식을 의미한다. 일반적으로 객체 속성 수, 함수 파라미터 수 등이 비교 대상보다 많을 경우, 구조적으로 더 큰 타입이라고 볼 수 있다.
아래 예시에서 User
타입은 name
, age
속성을 갖는다. 하지만 city
라는 추가 속성을 갖는 userThree
객체를 함수 파라미터로 넘겨도 에러가 발생하지 않는다.
type User = { name: string; age: number };
const saveUser = (user: User) => {};
const userOne = { name: 'One', age: 25 };
saveUser(userOne); // Ok
const userTwo = { name: 'Two' };
saveUser(userTwo); // ts(2345) Error! 'age' 속성이 '{ name: string; }' 형식에 없지만 'User' 형식에서 필수입니다
const userThree = { name: 'Three', age: 35, city: 'Seoul' };
saveUser(userThree); // OK
구조적 타입 시스템에선 기본적으로 타입 A
가 B
의 슈퍼셋인 경우(A
는 B
의 모든 속성을 포함하고 추가 속성도 가질 때) A
를 B
에 할당할 수 있다. (공변성, 반공변성과는 별도의 개념)
A
는 서브셋 B
에 할당할 수 있다B
는 슈퍼셋 A
에 할당할 수 없다