ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 타입스크립트- 3장 타입추론 (2)
    TypeScript 2022. 4. 10. 12:35

    아이템 20. 다른 타입에는 다른 변수 사용하기 

    한 변수를 다른 타입으로 재사용해도 되는 자바스크립트와 달리 타입스크립트는 에러가 난다.

    let id = '12-34-56';
    fetchProduct(id); // string으로 사용 
    id = 123456; 
    // '123456' 형식은 'string'형식에 할당할 수 없습니다.
    fetchProductBySerialNumber(id); // number로 사용
    //'string' 형식의 인수는 'number' 형식의 매개변수에 할당될 수 없습니다.

     

    이때 에러를 고치기 위해서는  id가 string과 number를 모두 포함할 수 있도록 string | number로 유니온 id 타입을 사용해 확장할 수 있다.

    let id: string|number = '12-34-56';
    fetchProduct(id); 
    id = 123456;  // 정상
    fetchProductBySerialNumber(id); // 정상

     

    하지만 이는 더 많은 문제를 야기할 수 있다. 

    id를 사용할 때마다 값이 어떤 타입인지 확인해야 하기 때문에 유니온 타입은 string이나 number 같은 간단한 타입에 비해 다루기 더 어렵다.

    차라리 별도의 변수를 도입하는 것이 낫다.

    const id = '12-34-56';
    fetchProduct(id); 
    const serial = 123456;  
    fetchProductBySerialNumber(serial);

     

    위 예처럼 다른 타입에는 별도의 변수를 사용하는게 바람직한 이유는 아래와 같다.

    1. 변수명을 더 구체적으로 지을 수 있다.
    2. 타입 추론을 향상시키며 타입 구문이 불필요해진다.
    3. 타입이 좀 더 간결해진다.(string | number 대신 string과 number를 사용)
    4. let 대신 const 변수를 선언하게 된다. (const로 변수 선언 시 코드 타입이 바뀌는 변수는 피해야 하며 목적이 다른 곳에는 별도의 함수명을 사용해야 한다)

    그런데 위의 재사용되는 변수를 아래의 예제처럼 '가려지는(shadowed)'변수를 혼동해서는 안된다.

    let id: string = '12-34-56';
    fetchProduct(id); 
    
    {
    	id = 123456;  // 정상
    	fetchProductBySerialNumber(id); // 정상
    }

     

    여기서 id는 이름이 같지만 실제로 아무런 관련이 없어 각기 다른 타입으로 사용돼도 무방하다. 

    하지만 이는 동작이 될지라도 사람들에게 혼란을 줄 수 있으므로 (목적이 다른 곳에는 꼭 별도의 변수명을 쓰자) 많은 개발팀에서 린터 규칙을 통해 '가려지는' 변수를 사용하지 못하게 한다. 

     


    요약 

    • 변수의 값은 바뀔 수 있지만 타입을 바뀌지 않는다.
    • 혼란을 막기 위해 타입이 다른 값을 다룰 때에는 변수를 재사용하지 않도록 한다. 

    아이템 21. 타입 넓히기 

    상수를 사용해서 변수를 초기화할 때 타입을 명시하지 않으면 타입 체커는 타입을 결정해야 한다. 

    즉 지정된 단일 타입을 가지고 할당 가능한 값들의 집합을 유추해야 한다는 뜻인데, 이러한 과정을 '넓히기(widening)'이라고 부른다.

     

    다음 예제는 실행은 잘되지만 편집기에서는 오류가 발생한다.

    interface Vector3 { x: number; y: number; z: number; }
    function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
        return vector[axis];
    }
    
    let x = 'x';
    let vec = { x: 10, y: 20, z: 30 };
    getComponent(vec, x);
    // 'string' 형식의 인수는 'x' | 'y' | 'z' 형식의 매개변수에 할당될 수 없습니다.

    getComponent 함수는 두 번째 매개변수에 'x' | 'y' | 'z' 타입을 기대했지만, x의 타입은 할당 시점에 넓히기가 동작해서 string으로 추론되어 오류가 난 것이다. (string 타입은 'x' | 'y' | 'z' 타입에 할당 불가)

     

    타입 넓히기가 진행될 때, 주어진 값으로 추론 가능한 타입이 여러개이기 때문에 상당히 모호해진다. 

    다음 예제를 보자.

    const mixed = ['x', 1];

    mixed의 타입이 될 수 있는 후보들은 상당히 많다. ('x' | 1)[] / ['x', 1] / [string, number] / (string|number)[] 등등.

    (위 경우에는 타입스크립트는 mixed의 타입을 (string|number)[]로 추론한다.)

     

    타입스크립트는 넓히기의 과정을 제어할 수 있도록 몇 가지 방법을 제공한다.

     

    첫 번째 방법은 const이다. 

    let대신 const로 변수를 선언하면 더 좁은 타입이 된다. 

     

    위의 getComponent 예시에서 const를 사용하면 이제 오류가 나지 않는다.

    interface Vector3 { x: number; y: number; z: number; }
    function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
        return vector[axis];
    }
    
    const x = 'x';
    let vec = { x: 10, y: 20, z: 30 };
    getComponent(vec, x); // 정상

    이제 x는 재할당될 수 없으므로 타입스크립트는 의심의 여지 없이 더 좁은 타입('x')로 추론할 수 있다.

    (또한 문자 리터럴 타입 'x'는 'x' | 'y' | 'z' 에 할당 가능하므로 타입체커를 통과한다)

     

    그러나 객체나 배열의 경우 const 사용 시 문제가 있다.

    const v = {
        x: 1,
    };
    
    v.x = 3;
    v.x = '3';
    v.y = 4;
    v.name = 'Pythagoras';

     

    v의 타입 역시 구체적인 정도에 따라 다양하게 추론될 수 있다.

    가장 구체적인 경우는 {readonly x: 1} 일 것이고, 가장 추상적인 경우는 {[key: string]: number} 혹은 object일 것이다.

     

    하지만 타입스크립트는 객체의 경우 각 요소를 let으로 할당된 것처럼 다룬다.

    그래서 v의 타입은 {x:number}가 된다. 

    덕분에 v.x에 다른 숫자를 재할당할 순 있지만 다른 타입을 할당할 순 없고,

    객체 v에 다른 프로퍼티를 추가하는 것도 불가능하다.

     

    const v = {
        x: 1,
    };
    
    v.x = 3;
    v.x = '3'; // '3' 형식은 'number' 형식에 할당할 수 없습니다.
    v.y = 4; // '{ x: number }' 형식에 'y' 속성이 없습니다.
    v.name = 'Pythagoras'; // '{ x: number }' 형식에 'name' 속성이 없습니다.

    타입스크립트는 잘못된 추론을 하지 않기 위해 적당한 강도의 타입 넓히기를 수행하는데,

    이 강도를 제어하기 위해 세 가지 방법이 존재한다.

     

    1. 첫 번째, 명시적 타입 구문을 제공하기
    2. 타입 체커에 추가적인 문맥 전달(함수의 매개변수로 값을 전달하는 등의 방식)
    3. const 단언문(as const) 사용하기
    // 명시적 타입 구문 제공하기
    const v: { x: 1|3|5 } = {
        x: 1,
    }; // 타입이 { x: 1|3|5; }
    
    // const 단언문 사용하기
    const v1 = {
        x: 1,
        y: 2,
    }; // 타입은 { x: number; y: number; }
    
    const v2 = {
        x: 1 as const,
        y: 2,
    }; // 타입은 { x: 1, y: number }
    
    const v3 = {
        x: 1,
        y: 2,
    } as const; // 타입은 { readonly x: 1; readonly y: 2; }

    배열을 튜플 타입으로 추론할 때도 마찬가지로 as const를 사용할 수 있다.

    const a1 = [1, 2, 3]; // 타입이 number[]
    const a2 = [1, 2, 3] as const; // 타입이 readonly [1, 2, 3]

    이렇듯, as const를 사용하면 타입스크립트가 최대한 좁게 타입을 추론한다는 것을 알 수 있다.

     

    요약 

    • 타입스크립트 넓히기를 통해 상수의 타입을 추론하는 법을 이해해야 한다.
    • 동작에 영향을 줄 수 있는 방법인 const, 타입구문, 문맥, as const에 익숙해져야 한다. 

     

    댓글

Designed by Tistory.