TypeScript

이펙티브 타입스크립트- 3장 타입추론 (2)

devSoo 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에 익숙해져야 한다.