Effective Typescrpt (Day 7)

d_fe·2022년 12월 13일
post-thumbnail

3장. 타입 추론

타입스크립트는 타입 추론을 적극적으로 수행한다.
이 타입 추론은 수동으로 명시해야 하는 타입 구문의 수를 매우 줄여 주기 떄문에, 코드의 전체적인 안정성이 향상된다.

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

타입스크립트는 결국 타입을 위한 언어이기 때문에, 변수를 선언할 때마다 타입을 명시해야 한다고 생각할 수 있지만, 사실 많은 타입 구문은 불필요하다.

//example 1
let x:number = 1;
let x = 1; // 이렇게만 써도 x의 타입을 number로 추론한다.

//example 2
const person: {
    name: string;
    born: {
        where :string;
        when: string;
    }
    died: {
        where: string;
        when: string;
    }
} = {
    name: 'Sodd',
    born: {
        where: 'NY',
        when: 'c.111'
    },
    died: {
        where : 'NT',
        when: 'Nbv. 120'
    }
}

const person1 = {
    name: 'Sodd',
    born: {
        where: 'NY',
        when: 'c.111'
    },
    died: {
        where : 'NT',
        when: 'Nbv. 120'
    }
} // 이렇게만 써도 충분하다.

➡️ 타입 추론은 우리보다 더 정확하게 작동할 때도 있다.

const axis1 : string = 'x'; // 타입은 string
const axis2 = 'y' // 타입은 y

// axis2의 타입이 string으로 생각할 수 있으나 'y'가 더 정확한 타입이다.

➡️ 타입이 추론되면 리팩터링 역시 용이해진다.

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

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

위 코드에서 id에 문자도 들어 있을 수 있음을 나중에 알게 되어 Product 내의 id 타입을 변경하면 함수안의 id의 타입과 맞지 않아 오류가 발생한다.

이는 함수안에 명시적 타입 구문이 있어 발생한 오류로 해당 부분을 없애면 통과하지만, 이 경우 비구조화 할당문을 이용하는 게 좋다.

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

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

정보가 부족해 타입이 추론하지 못하는 경우엔 명시적 타입 구문이 필요하지만, 이 경우 product에서 해당 상황을 해결하였으니 함수 안에 추가적으로 명시적 타입 구문을 넣는 것은 불필요하다.

➡️ 이상적인 타입스크립트 코드는 함수/메서드 시그니처에 타입 구문을 포함하지만, 함수 내에서 생성된 지역 변수에는 타입 구문을 넣지 않는다.

타입 구문을 생략하는 경우도 있는데

  1. 함수 매개변수에 기본값이 있는 경우
  2. 타입 정보가 있는 라이브러리를 사용하는 경우 (이 경우 콜백 함수의 매개변수 타입은 자동으로 추론된다,)
// 아래와 같이 쓸 필요가 없다.
app.get('/health',(request: express.Request, response: express.Response) => {
    response.send('OK')
})

//이렇게 쓰자.
app.get('/health',(request,response) => {
    response.send('OK')
})

✔️ 타입이 추론될 수 있음에도 여전히 타입을 명시하고 싶은 몇 가지 상황이 있다.

  1. 객체 리터럴을 정의할 때
// 이렇게 쓰면 잉여 속성 체크가 동작하지 않아 변수가 사용되는 순간에 오류가 발생한다.
const elmo = {
    name: 'Tick',
    id: '123213',
    price: 292.11,
}

logProduct(elmo) // 여기 elmo서 오류

// 하지만 명시적으로 타입을 지정해주면 잉여 속성 체크가 동작해 할당되는 순간에 오류가 발생한다.
const furby: Product = {
  name: 'Trinck',
  id: 11111, // 여기서 타입 오류
  price: 123,
}

logProduct(furby);
  1. 함수의 반환에도 타입을 명시하여 오류를 방지할 수 있다.
    타입 추론이 가능할지라도 구현상의 오류가 함수를 호출한 곳까지 영향을 미치지 않도록 하기 위해 명시하는 게 좋다.
// 조회 종목을 다시 요청하지 않도록 캐시한 함수
const cache: {[ticker: string] : number} = {};
function getQuote(ticker:string) {
    if(ticker in cache) {
        return cache[ticker]
    } 
    return fetch('https://quotes.example.com/?q=${ticker}')
            .then(res => res.json())
            .then(quote => {
                cache[ticker] = quote;
                return quote;
            })
}

getQuote는 항상 Promise를 반환해야 하므로 if구문에는 cache[ticker] 가 아니라 Promise.resolve(cache[ticker]) 를 반환해야 한다.

위 오류는 getQuote 내부가 아닌 호출한 코드에서 발생한다.
따라서 의도된 반환 타입을 명시하여 정확한 위치에 오류가 발생하게 만들 수 있다.

❗구현상의 오류가 사용자 코드의 오류로 표시되는 경우를 방지한다.

function getQuote(ticker:string) : 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}
}

위에서 add 함수의 반환 타입은 {x: number; y:number;} 로 추론되어 Vector2D 와 호환되지만, Vector2D로 나오지 않아 사용자 입장에서 혼란을 야기한다.

따라서 추론된 반환 타입이 복잡해질수록 명명된 타입을 제공하는 이점이 커진다.

☑️ TS가 타입을 추론할 수 있다면 타입 구문을 작성하지 않는 것이 좋다.
☑️ 이상적인 경우 함수/메서드 시그니처에는 타입 구문이 있지만, 함수 내 지역 변수에는 없다.
☑️ 추론될 수 있는 경우라도 객체 리터럴과 함수 반환에는 타입 명시를 고려할 것.


item 20. 다른 타입에는 다른 변수 사용하기

JS에서는 한 변수를 다른 목적을 가진 다른 타입으로 재사용해도 된다.

let id = 'adf'
fetchProduct(id) // id의 type은 string

id = 123;
fetchProductBySerialNum(id) // id의 type은 number

하지만 TS에서는 두 가지 오류가 발생한다.

let id = 'adf'
fetchProduct(id) // id의 type은 string

id = 123; // error1 > string형식에 할당할 수 없음
fetchProductBySerialNum(id) // error2 > string형식의 인수는 number 형식의 매개변수에 할당 불가

여기서 '변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다' 는 관점을 알 수 있다.

위 에러를 해결하려면 string 과 number를 모두 포함할 수 있도록 타입을 확장하면 된다.
(유니온 타입)

let id: string | number = '12';

하지만, 위와 같이 유니온 타입을 사용하면 id를 사용할 때마다 매번 값이 어떤 타입인지 확인해야 하기 때문에 차라리 별도의 변수를 도입하는 것이 낫다.

const id = '12' ;
const serial = 123123;

아무 관련 없는 값을 같은 변수에 무분별하게 재사용하면 혼란을 야기한다.
➡️별도의 변수를 사용하는 것이 바람직한 이유는 다음과 같다.

  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 좀 더 간결해진다.
  • let 대신 const로 변수를 선언하게 된다.

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

profile
오늘보다 내일 더 성장하는 프론트엔드 개발자가 되기 위해

0개의 댓글