TypeScript

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

devSoo 2022. 4. 3. 14:47

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

타입스크립트를 처음 접한 개발자는 변수를 선언할 때마다 타입을 명시해야 한다고 생각하여 모든 변수에 타입을 선언하는 실수를 자주 범한다.

 

//X
let x:number = 12;

다음처럼만 해도 충분하다. 
//O
let x = 12;
편집기에 x에 마우스를 올려 보면, 타입이 이미 number로 추론되어 있음을 확인할 수 있다. 
타입 추론이 된다면 명시적 타입 구문은 필요하지 않다.

 

아래 예시를 보자.

Product 타입과 기록을 위한 함수를 작성했는데 나중에 id에 문자가 들어 있음을 알게 됐다.

interface Product {
    id: number;
    name: string;
    price: number;
}

interface Product {
    id: string; // 아래의 오류를 발생시킨다.
    name: string;
    price: number;
}

function logProduct(product: Product) {
	const id: number = product.id;
    // ~~ 'string'형식은 'number' 형식에 할당할 수 없다.
    const name: string = product.name;
    const price: number = product.price;
    console.log(id,name,price);
}

 

사실 logProcut 함수 내의 명시적 타입 구문이 없었다면 코드는 아무런 수정 없이도 타입 체커를 통과했을 것이다.

 

비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 한다.

interface Product {
    id: string;
    name: string;
    price: number;
}

function logProduct(product: Product) {
    const { id, name, price } = product;
    console.log(id, name, price);
}

여기에 추가적으로 명시적 타입 구문을 넣는다면 불필요한 타입 선언으로 코드가 번잡해진다.

 

매개변수에 기본값이 있는 경우 타입은 해당 기본값의 타입으로 추론한다.

function parseNumber(str:string, base=10) {
//...
//여기서 기본값이 10이기 때문에 base 타입은 number로 추론된다.
}

보통 타입 정보가 있는 라이브러리에서, 콜백 함수의 매개변수 타입은 자동으로 추론된다. 

//X
app.get('health', (request: express.Request, response: express.Response) => {
	response.send('OK');
}

//O
app.get('health', (request, response) => {
	response.send('OK');
}

 

반대로 의도적으로 타입을 명시해주는 게 좋은 상황도 있다.

그 중 하나는 객체 리터럴을 정의할 때이다.

const elmo: Product = {
    name: 'Tickle Me Elmo',
    id: 481880627152,
    price: 28.99
}; // id는 string이어야 하므로 여기서 타입 오류 발생

const furby = {
    name: 'Furby',
    id: 630509430963,
    price: 35,
}; // 타입 구문이 없으므로 타입 오류 발생 X

logProduct(furby); // 여기서(객체가 사용되는 곳) 타입 오류 발생
위와 같이 타입을 명시하면 잉여 속성 체크가 동작하여 객체를 선언한 곳(실수가 실제로 발생한 곳)에서 타입 오류가 발생한다. 
반대로 타입을 명시하지 않으면 객체가 사용되는 곳에서 타입 오류가 발생한다. 

 

유사한 이유로 함수를 정의할 때는 반환 타입을 명시해주는 것이 좋다.

주변 시세를 조회하는 함수를 작성했다고 가정하고 이미 조회한 종목을 다시 요청하지 않도록 캐시를 추가해보자.

function getQuote(tikcer:string) {
return fetch(`https://quotes.example.com/?q=${ticker}`)
		.then(response => response.json());
       

// 이미 조회한 종목을 다시 요청하지 않도록 캐시 추가 

const cache = {[ticker:string]: number} = {};

function getQuote(ticker:string) {
	if(ticker in cache) {
    	return cache[ticker]; // 여기서 Promise.resolve(cache[ticker])가 반환되어야 함 
    }
    return fetch(`https://quotes.example.com/?q=${ticker}`)
		.then(response => response.json())
        .then(quote => {
        		cache[ticker] = quote;
                return quote;
             });
}

하지만 실행 시 오류는 getQuote 내부가 아닌 getQuote를 호출한 코드에서 발생한다. 

이때 의도된 반환 타입(Promise<number>)을 명시한다면, 정확한 위치에 오류가 표시되며, 
구현상의 오류가 사용자 코드의 오류로 표시되지 않는다. 

위의 이유 말고도 반환 타입을 명시해야 하는 이유가 두 가지 더 있다.

  1. 첫 째는 반환타입을 명시하면 함수에 대해 더욱 명확하게 알 수 있다.
    (반환 타입을 명시하려면 구현하기 전에 입력 타입과 출력타입이 무엇인지 알 아야 하기에 테스트 주도 개발과 비슷한 기능을 하게 된다)
  2. 명명된 타입을 사용하기 위해서 명시를 해줘야 한다. 
interface Vector2D { x: number; y: number; }
function add(a: Vector2D, b: Vector2D) {
    return { x: a.x + b.x, y: a.y + b.y };
} // 반환 타입이 Vector2D가 아니라 { x: number; y: number; }로 나온다.

이렇게 작성할 경우, add 함수의 반환 타입이 Vector2D가 아니라 { x: number; y: number; } 로 나온다.

이렇게 되면 입력은 Vector2D인데 출력이 Vector2D가 아니므로 당황스러울 수 있다.

그래서 반환 타입을 Vector2D로 명시해주는 것이 더 직관적이다.

 

ESLint 규칙 중에 'no-inferrable-types'를 사용하면 작성된 모든 타입 구문이 정말로 필요한지를 확인할 수 있다.

 


요약 

  • 타입스크립트가 타입 추론을 할 수 있다면 타입 구문을 작성하지 않는 게 좋다.
  • 이상적인 경우 함수/메서드의 시그니처에는 타입 구문이 있지만, 함수 내의 지역 변수에는 타입 구문이 없다.
  • 추론될 수 있는 경우라도 객체 리터널과 함수 반환에는 타입 명시를 고려해야 한다. 
    이는 내부 구현의 오류가 사용자 코드 위치에 나타나는 것을 방지해 준다.