자바스크립트는 덕 타이핑(duck typing) 기반이고 타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용합니다.
타입스크립트가 구조적 타이핑을 도입한 이유는 동적 타입 언어인 자바스크립트를 기반으로 하기 때문입니다.
Java 기반의 객체지향에서는 명목적 타이핑을 사용합니다.
구조적 타이핑이란?
구조적 타이핑은 값 자체의 타입보다는 값이 가진 내부 구조에 기반해서 타입 호환성을 검사한다.
좋든 싫든 타입은 열려있습니다.
타입스크립트의 클래스 역시 구조적 타이핑 규칙을 따릅니다.
클래스의 인스턴스가 Java 기반의 객체지향과 다를 수 있습니다.
어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있을 겁니다. 타입은 봉인되어 있지 않습니다.
추가 속성이 있으면 값의 집합은 더 작아집니다.
반대로 유니온 타입이 있으면 값의 집합은 더 커집니다.
내부 구조만 같다면 타입은 동일한 것으로 판단합니다.
classClassA{
str:string;constructor(str:string){this.str = str;}}const classA =newClassA('instance of A');const objectB: ClassA ={ str:'object literal'};// ---classA{clap(){console.log('A');}}classB{clap(){console.log('B');}}constC={clap(){console.log('C');},};let a:A=newA();
a.clap();
a =newB();
a.clap();
a =C;
a.clap();
a ={clap:()=>console.log('D')};
a.clap();
인터페이스의 이름이 달라도 내부 구조가 같다면 할당 가능합니다.
이는 Java 기반의 객체지향과의 큰 차이점이며 우리가 구조적 타이핑을 잘 이해하고 사용해야 하는 이유 입니다.
interfaceVector1D{
x:number;}interfacePoint{
x:number;}let p = v1;// Ok
v1 = p;// Ok
타입스크립트는 구조적 타이핑(덕 타이핑)을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있습니다
값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 합니다.
상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있습니다.
typeMeters=number&{ _brand:'meters'};typeSeconds=number&{ _brand:'seconds'};constmeters=(m:number)=> m as Meters;constseconds=(s:number)=> s as Seconds;const oneKm =meters(1000);// 타입이 Metersconst oneMin =seconds(60);// 타입이 Seconds
잉여 속성 체크는 오류를 찾는 효과적인 방법이지만, 타입스크립트 타입 체커가 수행하는 일반적인 구조적 할당 가능성 체크와 역할이 다릅니다. 할당의 개념을 정확히 알아야 잉여 속성 체크와 일반적인 구조적 할당 가능성 체크를 구분할 수 있습니다.
잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡는 데 효과적인 방법입니다. 선택적 필드를 포함하는 타입에 특히 유용한 반면, 적용 범위도 매우 제한적이며 오직 객체 리터럴에만 적용됩니다.
잉여 속성 체크에는 한계가 있습니다. 임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억해야 합니다.
interfaceVector1D{
x:number;}interfaceVector2DextendsVector1D{
y:number;}let v1: Vector1D ={
x:1,};let v2: Vector2D ={
x:1,
y:2,};
v1 = v2;// OK(임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다)
v2 = v1;// Property 'y' is missing in type 'Vector1D' but required in type 'Vector2D'.(2741)// ---interfaceA{
a:string;
b:string;}const a:A={
a:'a',
b:'b',
c:'c',// '{ a: string; b: string; c: string; }' 형식은 'A' 형식에 할당할 수 없습니다. 개체 리터럴은 알려진 속성만 지정할 수 있으며 'A' 형식에 'c'이(가) 없습니다.ts(2322)};const b ={
a:'a',
b:'b',
c:'c',};const c:A= b;// OK(임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다)
그렇다면 타입을 어떻게 사용해야 할까요?
공통적인 것을 원본 모델로 만들고 이를 유틸리티 타입을 활용해서 파생된 타입을 사용합니다.
타입스크립트는 절충하는 언어입니다.
원본 타입과 유틸타입의 조합으로 타입이 많아지는 것을 방지합니다.
리액트에서 컴포넌트를 만드는 것처럼 타이핑을 해보겠습니다.
원본 모델(인터페이스)과 타입 레이어의 분리합니다.
interface는 병합이 가능하다는 특징을 활용해 외부와의 소통에 활용합니다. 사용하는 곳에서 필요에 따라 유틸리티 타입 혹은 병합 성질을 활용해서 사용합니다.
type은 파일 내에서 interface를 활용해서 만들어서 사용합니다.
원본 모델은 css-in-js나 custom hook에서 거의 같지만 조금 다르게 활용되는 경우가 많습니다. 보통 Component의 props에서는 optional인 값이 css-in-js로 넘어갈 때는 필수값이 됩니다. 이를 위해 타입을 2번 만드는 것이 아니라 원본 모델과 유틸리티 타입을 활용해 타입을 여러개 만들지 않고 유지보수성도 향상 시킬 수 있습니다.
타입스크립트의 핵심 개념인 구조적 타입 시스템을 기반으로 설명하면, 타입스크립트에서 구조적 타입 시스템을 선택한 이유는 다음과 같습니다. interface는 가장 기저에 있는 객체의 모델을 나타냅니다. 다만, 자바스크립트는 타입으로 정의할 수 없는 다양한 형태가 런타임으로 동작될 수 있습니다. 그렇기에 런타임과 컴파일 타임 (사실 타입 체킹 타임 이겠죠?)이 다르고, 런타임에서 원하는 (허용해야하는) 경우와 오브젝트에서 은닉하고 캡슐화 해야하는 경우 등이 존재. 그러한 다양한 타입을 포용하려면 타입을 자유롭게 가공하고 만들 수 있도록 제공되어야 했습니다. 그렇기에 다양한 Utility Type과 그에따른 순수 객체를 위한 인터페이스를 분리하고 조합하여 Type Alias를 하여금 더 복잡한 타입을 나타낼 수 있도록 하였고, 궁극적인 지향점은, 아주 최소한의 모델을 만들고 최소한의 모델을 유연하게 타입으로 만들어서, 타입을 조합해 최소한의 타입으로 동작하도록 하는 것입니다. 또한 이 구조는 타입스크립트에서 지향하고 있는 “점진적 타이핑“과 자바스크립트의 “덕 타이핑” 관점에서도 충분히 먹히는 선택지기에 가게 되었죠. (출처: 타입스크립트 위키피디아 + 타입스크립트 깃헙 MR)
서브타입 파보기
extends에서의 관계
서브셋이 아닌 경우
// ERRORinterfaceA{
a:string;}interfaceBextendsA{// Interface 'B' incorrectly extends interface 'A'. Types of property 'a' are incompatible. Type 'number' is not assignable to type 'string'.(2430)
a:number;}
// ERRORinterfaceA{
a:string;}interfaceBextendsA{// Interface 'B' incorrectly extends interface 'A'. Types of property 'a' are incompatible. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.(2430)
a?:string;}
선언 병합에서는?
같은 타입인 경우
// OKinterfaceA{
a:string;}interfaceA{
a:string;}
서브셋이 아닌 경우
// ERRORinterfaceA{
a:string;// All declarations of 'a' must have identical modifiers.(2687)}interfaceA{
a:number;// All declarations of 'a' must have identical modifiers.(2687)// Subsequent property declarations must have the same type. Property 'a' must be of type 'string | undefined', but here has type 'string'.(2717)}
서브셋인 경우
// ERRORinterfaceA{
a:string;// All declarations of 'a' must have identical modifiers.(2687)}interfaceA{
a?:string;// All declarations of 'a' must have identical modifiers.(2687)// Subsequent property declarations must have the same type. Property 'a' must be of type 'string', but here has type 'string | undefined'.(2717)}
서브셋인 경우2
// ERRORinterfaceA{
a:string;}interfaceA{
a:'a';// Subsequent property declarations must have the same type. Property 'a' must be of type 'string', but here has type '"a"'.(2717)}
집합론적으로 타입 바라보기
구조적 타이핑이란? 내용을 다시 한번 읽고 보시면 좋습니다.
타입을 공변의 입장에서 바라봅니다.
할당 가능성을 중심으로 살펴봅니다.
타입을 개념으로 바라보는지 원소로 바라보는지에 따라서도 차이가 있습니다.
number는 숫자의 슈퍼 타입이다.
number > 1
// ERRORconst a:number=1;const b:1= a;// Type 'number' is not assignable to type '1'.(2322)// OKconst c:1=1;const d:number= c;
readonly Array는 Array의 슈퍼 타입이다.
readonly Array > Array
// ERRORconst a:readonly[1]=[1];const b:[1]= a;// The type 'readonly [1]' is 'readonly' and cannot be assigned to the mutable type '[1]'.(4104)// OKconst c:[1]=[1];const d:readonly[1]= c;
객체를 인자로 넘기는 경우는 어떤가
functiona(obj:{ b:string}){return obj;}// OKconst c ={
b:'1',};a(c);// OK(임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다)const d ={
b:'1',
e:'1',};a(c);a({ b:'1'});a({ b:'1', e:'1'});// Argument of type '{ b: string; e: string; }' is not assignable to parameter of type '{ b: string; }'. Object literal may only specify known properties, and 'e' does not exist in type '{ b: string; }'.(2345)
일반적으로 객체에 속성이 더 있다면 잉여 속성을 체크하지 않아서 문제가 없습니다. 하지만 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행됩니다. 이것은 구조적 타이핑의 한계를 제한하기 위해 도입된 것이므로 구조적 타이핑 입장에서의 집합론에서는 잉여 속성이 있어도 서브 타입으로 보는 것이 맞습니다.
인터페이스로 바라보기
interfaceA{
a:string|undefined;}// OKinterfaceBextendsA{
a:string;}// OKinterfaceCextendsA{
a:undefined;}interfaceD{
a:undefined;}// ERRORinterfaceEextendsD{
a:string;}// 특이하게 바라보기interfaceF{
a:string;
b:string;}typeG= Omit<F,'a'>;let f:F={
a:'a',
b:'b',};let g:G={
b:'b',};// OK
g = f;// Property 'a' is missing in type 'G' but required in type 'F'.(2741)
f = g;interfaceH{
a:string;}interfaceI{
b:string;}// 요 개념이 아니라 interface J로 바라봐야 함typeJ=H&I;const h:H={
a:'a',};const i:I={
b:'b',};interfaceJ{
c:string;}interfaceHextendsJ{
a:string;}interfaceIextendsJ{
b:string;}