Object.keys 메서드의 타입 정의


타입스크립트를 사용하다 보면 아래 같은 상황을 자주 마주한다. 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


구조적 타이핑(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

구조적 타입 시스템에선 기본적으로 타입 AB의 슈퍼셋인 경우(AB의 모든 속성을 포함하고 추가 속성도 가질 때) AB에 할당할 수 있다. (공변성, 반공변성과는 별도의 개념)