본문 바로가기

IT

Effective Typescript - 19 - 추론 가능한 타입을 사용해 장황한 코드 방지하기

변수를 선언할 때마다 타입을 명시적으로 기입하는 것은 사실상 반드시 필요한 것은 아니다. 왜냐하면 타입스크립트 타입 체커는 타입 추론을 해주기 때문이다. 

 

저자는 타입 스크립트 고수와 하수의 차이를 아래와 같이 구분하고 있다.

고수 : 반드시 필요한 부분에만 타입을 명세해 주는 노하우를 가지고 있다. 타입 명세를 난발하지 않아 코드가 장황하지 않다.

하수 : 굳이 필요없는 타입 명세를 명시적으로 계속해서 코드가 장황해진다.

let x: number = 12;
let y = 12; // 이정도만 해도 충분하다.

때로는 타입스크립트 타입체커는 우리의 생각보다 좀 더 구체적으로 타입 추론을 한다. 아래의 예제를 보면 얼마나 구체적으로 타입 추론을 하는지 이해할 수 있다.

const axis1: string = 'x'; //타입은 'string'
const axis2 = 'y';// 타입은 'y' -> const로 선언했기 때문에 타입이 string이 아니게 된다.
const test : typeof axis2 = 'x'; // 오류이다.

함수 파라미터로 object를 할당받았을 때, 각각의 property에 대한 값 취득 시 비구조화 할당 문을 사용하면 타입을 명시하는 것보다 더 편리하다. 

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

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

function logProductFix(product: Product) {
    const {id, name, price} = product;
    //typeof id = number;
    //typeof name = string;
    //typeof price = number;
    console.log(id, name, price);
}

위와 같이 저자는 이상적인 타입 스크립트 코드는 함수 혹은 메서드 구현부에 타입 구문을 포함시키고, 함수 내 로컬 변수 할당 시 티입 구문을 일일이 명시하는 것은 불필요하다고 주장한다. 함수를 읽는 사람에게 로직에 대한 이해만 잘 될 수 있도록 집중시키는게 그 이유라고 한다.

 

한편, 타입 정보가 포함되어 있는 node.js 라이브러리 사용 시, 콜백 함수의 매개변수 타입은 자동으로 추론이 되기 때문에 이와 같이 외부 라이브러리의 콜백 함수를 정의할 때 매번 그 매개변수의 타입을 명시하는 것은 불필요한 일이라고 한다. 아래는 node express.js 라이브러리를 사용할 때의 예제로, 어떤 식으로 콜백 함수를 정의하는 게 추천되는 방식인지 보여준다.

//비추천
app.get('user_info', (req: express.Request, res: express.Response) =>{
    res.send('...');
});

//추천
app.get('user_info', (req, res) =>{
    res.send('...');
});

 

다음으로, 타입 체커에 의해 타입이 추론 가능한 상황임에도 불구하고 타입을 명시하고 싶은 상황에 대해 고민해보자.

그 상황 중 하나로 객체 리터럴을 정의할 때가 될 수 있는데, 잉여 속성 체크 기능의 도움을 받는 상황이라 할 수 있다.

//객체 리터럴을 할당할 때, 타입체커의 '잉여속성 체크' 기능을 활용 하고 싶다면,
//할당 대상 변수에 타입을 명세하는 것이 도움된다.

const demo_product: Product = {
    name: 'iPod air 5',
    price: 400,
    id: 15488
}

const demo_product2: Product = {
    name: 'iPod air 5',
    priCe: 400, // (!!오류)타입을 명시함으로서 의도하지 않은 오타에 대한 방지 가능 (잉여 속성 체크)
    id: 15488
}

이밖에, 함수의 리턴 타입에 타입을 명시하면 미리 오류를 방지하는데 도움이 된다.

const cache: {[ticker: string]: number } = {};// 조회한 종목을 캐시한다.

function getStockInfo(ticker: string){ // 위 함수의 return type = string | Promise<any>
    if(ticker in cache){
        return cache[ticker];
    }

    return fetch(`https://stock.seekingalpha.com?ticker=${ticker}`)
    .then(res=> res.json())
    .then(stock_info =>{
        cache[ticker] = stock_info.price;
        return stock_info.price;
    })
}

// 함수를 아래와 같이 호출 하여 사용할 경우 오류가 발생.
getStockInfo('AAPL').then((price)=>{
    console.log(price);
});

// return type을 Promise<number>로 통일한다면 의도하지 않은 값 반환을 예방 할 수있다.
function getStockInfoFix(ticker: string): Promise<number>{
    if(ticker in cache){
        return cache[ticker]; // 오류
    }

    return fetch(`https://stock.seekingalpha.com?ticker=${ticker}`)
    .then(res=> res.json())
    .then(stock_info =>{
        cache[ticker] = stock_info.price;
        return stock_info.price;
    })
}

함수 시그니처에 리턴 타입을 명시하면 코드를 읽는 사람이 그 함수의 동작에 대해 좀 더 이해하기 쉬워진다.

또한 명명된 타입을 의도적으로 사용하기 위함이라 하는데, 그 구체적 내용은 아래의 예제를 참고하자.

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

const add_vector = (a: Vector2D, b: Vector2D) =>{
    return {x: a.x + b.x, y: a.y + b.y};
}

type AddVector = typeof add_vector;
// type AddVector = (a: Vector2D, b: Vector2D) => {
//     x: number;
//     y: number;
// }
// !!!) 반환 타입이 Vector2D 라는 것을 의도했지만 결과는 다르다.

// 반환 타입을 명시적으로 'Vector2D'로 지정해줌으로서 코드의 일관성이 유지된다.
const add_vector2 = (a: Vector2D, b: Vector2D): Vector2D =>{
    return {x: a.x + b.x, y: a.y + b.y};
}

type AddVectorFixed = typeof add_vector2;
// type AddVectorFixed = (a: Vector2D, b: Vector2D) => Vector2D