타입스크립트

타입스크립트의 구조적 타이핑과 잉여속성체크 이해하기

kk지모 2023. 11. 26. 23:22
이펙티브 타입스크립트 책(아이템 4, 아이템 11)을 정리한 글입니다. 

 

구조적 타이핑

자바스크립트는 함수의 매개변수 값이 요구사항을 만족한다면 타입이 무엇인지 신경 쓰지 않습니다. 

타입스크립트의 타입 시스템은 자바스크립트의 이런 런타임 동작을 그대로 모델링합니다.

interface Vector2D {
    x: number;
    y: number; 
}

interface NamedVector {
    name: string; 
    x: number; 
    y: number; 
}

function calculateLength(v: Vector2D) {
    return Math.sqrt(v.x * v.x + v.y * v.y);
}

const v: NamedVector = { name: 'Zee', x:3, y: 4 };

calculateLength(v); // 정상

 

위 예제에서 NamedVector의 구조가 Vector2D와 호환되기 때문에 calculateLength 호출이 가능합니다.

이것을 '구조적 타이핑(structural typing)'이라고 합니다.  

 

함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질 거라 생각하기 쉽습니다. 

좋든 싫든 타입은 열려 있고, 이러한 특성 때문에 가끔 당황스러운 결과가 발생합니다. 

interface Vector3D {
    x: number; 
    y: number; 
    z: number; 
}

function calculateLength1(v: Vector3D) {
    let length = 0; 

    for(const axis of Object.keys(v)) {
        const coord = v[axis]; 
                   // ~~~~~~~ 'string'은 'Vector3D'의 인덱스로 사용할 수 없기에 
                   //          엘리먼트는 암시적으로 'any' 타입입니다.  
        length += Math.abs(coord); 
    }

    return length; 
}

 

axis는 Vector3D 타입인 v의 키 중 하나이기 때문에 "x", "y", "z" 중 하나일 것이라고 생각할 수 있지만 v는 어떤 속성이든 가질 수 있기 때문에, axis의 타입은 string이 될 수도 있습니다. 그러므로 타입스크립트는 v[axis]가 어떤 속성이 될지 알 수 없기 때문에 coord의 타입이 number라고 확정할 수 없습니다. 

 

잉여 속성 체크

타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인합니다. 

리터럴 타입은 한 가지 값만 포함하는 타입입니다. 

type A = 'A';
type B = 'B';
type Twelve = 12;

 

interface Room {
    numDoors: number; 
    ceilingHeightFt: number; 
}

const r: Room = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present'
// ~~~~~~~~~~~~~~~~~~~~~ 객체 리더럴은 알려진 속성만 지정할 수 있으며 
//                      'Room' 형식에 'elephant'이(가) 없습니다.
}

const obj = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present'
}

const r2: Room = obj // 정상

 

Room 타입에 생뚱맞게 elephant 속성이 있는 것이 어색하긴 하지만, 구조적 타이핑 관점으로 생각해 보면 오류가 발생하지 않아야 합니다. 하지만 임시 변수 obj 객체를 도입해 보면, obj 객체는 Room 타입에 할당이 가능합니다. 

 

객체 리터럴을 사용하면 구조적 타입 시스템에서 발생할 수 있는 오류를 잡을 수 있도록 '잉여 속성 체크'라는 과정이 수행됩니다. 

잉여 속성 체크는 조건에 따라 동작합니다. 

 

선택적 속성만 가지는 '약한(weak)' 타입에도 비슷한 체크가 동작합니다. 

interface LineChartOptions {
    logscale?: boolean;
    invertedAxis?: boolean;
    areaChart?: boolean;
}

const opts = { logScale: true };

const o: LineChartOptions = opts; 
   // ~ '{ logScale: boolean; }' 유형에
   //   'LineChartOptions' 유형과 공통적인 속성이 없습니다.

 

구조적 관점에서 LineChartOptions 타입은 모든 속성이 선택적이므로 모든 객체를 포함할 수 있습니다. 이런 약한 타입에 대해서 타입스크립트는 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행합니다. 

 

잉여 속성 체크는 타입 단언문을 사용할 때에는 적용되지 않습니다. 

interface Options {
    title: string; 
    darkMode?: boolean;
}

const o = {
    darkmode: true, 
    title: 'Ski Free'
} as Options // 정상

 


 

끝!

구조적 할당 가능성 체크와 잉여 속성 체크를 구분하자  

잉여 속성 체크는 임시 변수를 도입하거나, 타입 단언문을 사용하면 동작하지 않는 한계가 있다는 것을 기억하자