Index
TL;DR!
타입스크립트는 자바스크립트의 덕 타입을 표현하기 위해 구조적 서브 타이핑을 채택하고 있다.
Object.keys()
는 런타임에서의 안정성을 위해 넓은 타입인 string[]
타입으로 추론된다.
문제
개발을 하다 보면 자연스럽게 객체를 많이 사용하게 된다. 이때 Object.keys 메서드를 사용하면 항상 key
값이 string
으로 추론되는 것을 확인할 수 있다.
string
으로 타입 추론(Type Inference) 되었기 때문에 이후 코딩의 편의성을 위해 다시 타입 단언(Type Assertion)을 하게 된다.
단언을 통한 타입 제어가 마음을 불편하게 하기 때문에 제너릭 타입으로라도 추론하고 싶어 진다.
하지만 타입스크립트는 Object.keys<T>() 제너릭 타입을 제공하지 않는다.
여기서 의문이 생긴다.
- 기존의 key 타입을 왜 추론하지 못할까?
- 제너릭 타입은 왜 제공하지 않았을까?
오늘은 타입스크립트를 사용하면서 만나는 미묘한 당혹스러움에 대해 파헤쳐 보고자 한다.
구조적 서브 타이핑이란
앞에서 다룬 문제의 이유는 타입스크립트가 구조적 서브 타이핑을 기반으로 하고 있기 때문이다.
이전에 우아한 타입스크립트에서 구조적 타이핑을 잠깐 이야기한 적이 있었다.
Image Source: What is duck typing
자바스크립트는 덕 타이핑을 기반으로 하는 동적 타이핑 언어이다.
따라서 타입스크립트는 자바스크립트의 특성(유연한 동적 타입)을 해치지 않으면서 타입을 강제(정적 타이핑)하기 위한 고민을 하게 된다.
위와 같은 객체 타입 Book
을 선언하게 되면 일반적인 명목적 타입 시스템에서는 반드시 Book { name: string }
형태의 타입만 와야 한다.
하지만 타입스크립트에서는 위와 같은 모든 형태의 객체가 가능하다. 이것이 바로 구조적 서브 타이핑이다.
구조적 타입 시스템의 주요 특성은 값을 할당할 때 정의된 타입에 필요한 속성을 가지고 있다면 호환된다는 것이다.
따라서 구조적 타입 시스템에서 타입은 값의 집합으로 생각하면 된다.
그렇다면 구조적 서브 타이핑과 Object.keys
의 반환 타입에는 어떤 연관이 있는 것일까?
자바스크립트의 덕 타입 덕에 객체는 런타임 단계에서 더 많은 속성을 가질 수 있다. 또한 구조적 서브 타이핑은 필요한 속성을 가지고 있다면 확장된 집합과 호환되며 에러를 노출하지 않는다.
그렇기 때문에 타입스크립트는 객체 인자에 T
타입의 값만 존재한다는 보장을 할 수 없다.
따라서 타입스크립트는 런타임에서의 안정성을 찾기 위해 좁은 타입의 (keyof T)[]
가 아닌 넓은 타입인 string[]
으로 추론한다.
관련 논의는 #12253 이슈 코멘트에서 확인할 수 있다.
- 글의 말미에서 구조적 서브 타이핑에 대해 더 다루도록 하겠다.
해결
이제 타입스크립트가 Object.keys
의 값을 string[]
으로 추론하는 이유를 확인했다.
이를 타입 단언을 사용하지 않고 추론하려면 어떻게 해야 할까?
타입 가드를 통한 타입 좁히기
타입 가드(Type Guards)를 통한 타입 좁히기(Type Narrowing)를 활용하면 타입 단언을 하지 않아도 적절하게 타입을 추론할 수 있게 된다.
무엇보다 런타임에서도 안전한 코드로 변화했다.
ONE MORE THING
구조적 서브 타이핑을 조금 더 알아보자.
Book
타입과 Car
타입이 유니온 혹은 교차 될 때 타입 추론이 혼란스러운 부분이 있다.
BookOrCar
는 { name: string }
혹은 { model: string }
타입이 되기 때문에 두 값이 공존해야 한다고 느껴진다. 하지만 타입스크립트는 두 값 모두 추론하지 못한다.
반대로 교차 타입은 BookAndCar
에서는 모든 값을 가지지만 AandB
에서는 never
타입이 추론된다.
앞에서 "구조적 타입 시스템에서의 타입은 값의 집합으로 생각하면 된다"고 했다.
각 타입을 값의 집합으로 나열해 보자.
Book | Car
의 경우에 Book 혹은 Car 중 "항상 존재하는 값"이 없는 것을 확인(name
혹은 model
이 반드시 있어야 하는 경우가 없음) 할 수 있다.
반면 Book & Car
의 경우 "항상 존재하는 값"이 있는 것을 확인할 수 있다.
결론
따라서 Book | Car
에서는 항상 존재하는 값이 없기 때문에 name
, model
어느 값도 존재하지 않게 되지만 Book & Car
에서는 name
, model
모두 항상 존재하기 때문에 두 값 모두 존재하게 된다.
마무리
타입스크립트를 사용하면서도 언어의 근본적인 철학을 이해하지 못한 상태로 작업한 것 같아 반성하게 되는 계기였다.
타입을 사용하면서 지금처럼 당혹스러운 부분이 있었는데 이번 기회에 많이 이해할 수 있었다.
참고