Front-end Developer

0%

타입스크립트 이펙티브 아이템 19 - 아이템 20

아이템 19 추론 가능한 타입을 사용해 장황한 코드 방지하기

타입스크립트가 결국 타입을 위한 언어이기 때문에 변수를 선언할 때마다 타입을 명시해야 한다고 생각하기 쉽다. 하지만 코드의 모든 변수에 타입을 선언하는 것은 비생산적이다. 타입스크립트는 타입 추론이 된다면 명시적 타입 구문은 필요하지 않다. let x = 12;와 같은 구문은 x가 number로 추론되기 때문에 굳이 let x: number = 12;로 작성하지 않아도 되고, 객체와 배열에 대해서도 동일하다. 아래 예시에서 1️⃣은 2️⃣로 작성해도 동일하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//1️⃣
const person: {
name: string;
born: {
where: string;
when: string;
};
died: {
where: string;
when: string;
};
} = {
name: 'Sojourner Truth',
born: {
where: 'Swartekill, NY',
when: 'c.1797',
},
died: {
where: 'Battle Creek, MI',
when: 'Nov. 26, 1883',
},
};

//2️⃣
const person = {
name: 'Sojourner Truth',
born: {
where: 'Swartekill, NY',
when: 'c.1797',
},
died: {
where: 'Battle Creek, MI',
when: 'Nov. 26, 1883',
},
};

때로는 추론이 더 정확할 때가 있는데, 아래의 경우 명시적으로 string이라고 타입을 준 것 보다 추론된 ‘y’가 사실은 더 정확하다.

1
2
const axis1: string = 'x'; // Type is string
const axis2 = 'y'; // Type is "y"

아래 예시에서 Product의 id를 number라고 작성했다가 나중에 문자도 있을 수 있다는 것을 알게되어 string으로 작성했다고 가정해보자. 이 경우 선언된 타입과 함수 내의 타입이 일치하지 않아서 오류를 발생시킨다. 만약 여기서 명시적 타입 구문이 없었다면 문제없이 타입 체커를 통과했을 것이다. 그래서 이런 경우에는 비구조 할당문으로 구현하는 것이 더 나은 선택이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Product {
id: string;
name: string;
price: number;
}

function logProduct(product: Product) {
const id: number = product.id;
// ~~ Type 'string' is not assignable to type 'number'
const name: string = product.name;
const price: number = product.price;
console.log(id, name, price);
}

//비구조 할당 - 여기에 추가로 명시적 타입 구문을 넣는 것은 불필요하다.
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const { id, name, price } = product;
console.log(id, name, price);
}

그러나 정보가 부족해서 타입스크립트가 스스로 판단하기 어려운 경우에는 명시적 타입 구문이 필요하다. 위 예제에서 logProduct 함수에서 매개밴수의 타입을 Product로 명시한 경우가 그 예이다. 이상적인 타입스크립트 코드는 함수/메서드 시그니처에 타입 구문을 포함하지만, 함수 내에서 생성된 지역변수에는 타입 구문을 넣지 않는 것이다. 단, 기본값이 있는 경우에는 타입 구문을 생략하기도 한다.

1
2
3
function parseNumber(str: string, base = 10) {
// ...
}

타입이 추론될 수 있음에도 타입을 명시하고 싶은 경우가 있다. 객체 리터럴의 정의과 함수의 반환 타입을 명시할 때이다.

객체 리터럴의 정의

객체 리터럴에서 타입을 명시하면 잉여 속성 체크가 동작해서 실제로 실수가 방생한 부분에 정확하게 오류를 표시해 줄 수 있다. 아래 예시에서 타입 구문을 제거하면 잉여 속성 체크가 동작하지 않아서 실제 오류가 발생한 id쪽이 아니라 객체가 사용되는 곳에서 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Product {
id: string;
name: string;
price: number;
}

function logProduct(product: Product) {
const id: number = product.id;
// ~~ Type 'string' is not assignable to type 'number'
const name: string = product.name;
const price: number = product.price;
console.log(id, name, price);
}
const furby: Product = {
name: 'Furby',
id: 630509430963,
// ~~ Type 'number' is not assignable to type 'string'
price: 35,
};
logProduct(furby);

함수의 반환

타입 추론이 가능한 경우에도 구현상의 오류가 함수를 호출한 곳까지 영향을 미치지 않도록 하기 위함이다. 반환 타입을 명시하면 구현상의 오류가 사용자 코드의 오류로 표시되지 않고, 오류의 위치를 제대로 표시해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cache: { [ticker: string]: number } = {};
function getQuote(ticker: string) {
//getQuote가 반환하는 것은 Promise.resolve(cache[ticker])이어야 한다.
//따라서 여기에서 아래와 같은 오류가 발생해야 한다.
//~~~~~~~~~~~~~ Type 'number' is not assignable to 'Promise<number>'
//하지만 실제 오류는 가장 아래 getQuote를 호출한 코드에서 발생한다.
if (ticker in cache) {
return cache[ticker];
}
return fetch(`https://quotes.example.com/?q=${ticker}`)
.then((response) => response.json())
.then((quote) => {
cache[ticker] = quote;
return quote;
});
}
function considerBuying(x: any) {}
getQuote('MSFT').then(considerBuying);
// ~~~~ Property 'then' does not exist on type
// 'number | Promise<any>'
// Property 'then' does not exist on type 'number'

그 외에도 반환타입을 명시하는 것은 아래 두 가지 장점이 있다.

  • 함수를 더욱 명확하게 알기 쉽다. 반환 타입을 명시하려면 입력, 출력 타입에 대해 알아야 하고 미리 명시해야만 하기 때문에 타입을 미리 작성하여 구현에 맞추어 주먹구구식으로 타입이 작성되는 것이 아닌 테스트 주도 개발처럼 작성할 수 있게 된다.
  • 명명된 타입을 사용할 수 있다. 반환 타입을 명시하면 더욱 직관적인 표현이 되고, 반환 값을 별도의 타입으로 정의하면 타입에 대한 주석을 작설항 수 있어 함수에 대해 더 자세히 설명하게 된다.

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

자바스크립트에서는 한 변수를 다른 목적을 가지는 다른 타입으로 재사용해도 되는데, 타입스크립트에서는 이렇게 사용하면 두 가지 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//자바스크립트
let id = '12-34-56';
fetchProduct(id); //string으로 사용

id = 123456;
fetchProductBySerialNumber(id); //number로 사용

//타입스크립트
function fetchProduct(id: string) {}
function fetchProductBySerialNumber(id: number) {}
let id = '12-34-56';
fetchProduct(id);

id = 123456;
// ~~ '123456' is not assignable to type 'string'.
fetchProductBySerialNumber(id);
// ~~ Argument of type 'string' is not assignable to
// parameter of type 'number'

여기서 중요한 점은 변수의 값은 바뀔 수 있지만 타입은 바뀌지 않는다.는 점이다. 범위를 좁히는 방법으로 타입을 바꿀 수는 있지만 그것은 새로운 변수값을 포함하도록 확장하는 것이 아니라 타입을 더 작게 제한하는 것이다.

유니온 타입을 이용한 타입의 확장

id가 string, number를 모두 포함할 수 있도록 타입을 확장하는 유니온을 통해 아래와 같이 작성하면 에러는 해결된다. 하지만 이렇게 작성하면 매번 id값이 string인지 number인지 확인해야 하기 때문에 이런 경우에는 별도의 변수로 작성하는 것이 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fetchProduct(id: string) {}
function fetchProductBySerialNumber(id: number) {}
let id: string | number = '12-34-56';
fetchProduct(id);

id = 123456; // OK
fetchProductBySerialNumber(id); // OK

//유니온 타입 대신 별도의 변수로 작성
function fetchProduct(id: string) {}
function fetchProductBySerialNumber(id: number) {}
const id = '12-34-56';
fetchProduct(id);

const serial = 123456; // OK
fetchProductBySerialNumber(serial); // OK

무엇보다 변수를 재사용하는 방식은 타입 체커는 물론 사람에게도 혼란을 주기 때문에 지양해야 한다. 타입이 다른 경우 별도의 변수를 사용하는 것이 바람직한 이유는 다음과 같다.

  • 서로 관련이 없는 두 개의 값을 분리한다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 간결해진다.
  • let 대신 const로 변수를 선언하게 된다. 이렇게 하면 코드가 간결하고, 타입 체커의 타입 추론이 용이하다.

References
[이펙티브 타입스크립트] 댄 밴더캄 지음