[이펙티브 타입스크립트] 3장 타입 추론 - 1 (item 19 ~ 22) 본문

Web/TypeScript

[이펙티브 타입스크립트] 3장 타입 추론 - 1 (item 19 ~ 22)

미니모아 2023. 3. 16. 01:17
반응형

3장 타입 추론-1

타입스크립트는 타입 추론을 적극적으로 수행합니다. 타입 추론은 수동으로 명시해야하는 타입 구문의 수를 엄청나게 줄여주기 때문에, 코드의 전체적인 안정성이 향상됩니다.

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

타입스크립트가 타입을 추론할 수 있다면 명시적 타입 구문을 작성하지 않는 게 좋습니다.

let x: number = 12; // 비생산적이며 형편없는 스타일
let x = 12; // 이렇게만 해도 충분

편집기에서 x에 마우스를 올려보면 타입이 number로 이미 추론되어 있음을 확인할 수 있습니다.

만약 타입을 확신하지 못한다면 편집기를 통해 체크하면 됩니다. 더 복잡한 객체도 가능합니다.

// 거추장스러움
const person: {
    number: string;
    born: {
        where: string;
        when: string;
    };
    died: {
       where: string;
       when: string;
    };
} = {
    name: 'peter',
    born: {
        where:'busan';
        when:'1977';
    };
    died: {
       where:'seoul';
       when:'2000';
    };
}

// 타입 생략
const person = {
    name: 'peter',
    born: {
        where:'busan';
        when:'1977';
    };
    died: {
       where:'seoul';
       when:'2000';
    };
}

이상적인 경우 함수/메서드의 시그니처에는 타입 구문이 있지만, 함수 내의 지역 변수에는 타입 구문이 없습니다.

타입이 추론되면 리팩토링 역시 용이해집니다.

아래와 같이 product타입과 기록을 위한 함수가 있다고 가정해보겠습니다.

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
}

id에 문자도 들어올 수 있다는 사실을 나중에 알게되어 Product 내의 id의 타입을 변경한다면 logProduct 함수 내의 id 변수 선언에 있는 타입과 맞지 않기 때문에 오류가 발생합니다.

하지만 명시적 타입 구문이 없었다면, 코드는 아무런 수정 없이 타입 체커를 통과했을 것입니다.

logProduct는 비구조화 할당문을 통해 모든 지역 변수의 타입이 추론되도록 구현하는 게 더 낫습니다.

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

function logProduct(product: Product) {
  const {id, name, price} = product
}

정보가 부족해서 타입스크립트가 타입 추론이 어려울 경우에는 명시적 타입 구문이 필요합니다. logProduct 함수에서 매개변수 타입을 Product로 명시한 경우가 그 예입니다. 타입스크립트에서 변수의 타입은 최종 사용처를 고려하지 않고 일반적으로 처음 등장할 때 결정됩니다.

이상적인 타입스크립트 코드는 함수/메소드 시그니처에 타입 구문을 포함하지만 함수 내에서 생성된 지역 변수에는 타입 구문을 넣지 않습니다. 타입 구문을 생략하여 방해되는 것들을 최소화하고 코드를 읽는 사람이 구현 로직에 집중할 수 있게 하는 것이 좋습니다.

함수 매개변수에 타입 구문을 생략하는 경우도 간혹 있습니다.

// 기본값이 있는 경우
function parseNumber(str: string, base= 10){/** */}

// 타입 정보가 있는 라이브러리에서, 콜백함수의 매개변수 타입은 자동으로 추론된다.
// (x)
app.get('/health', (request:express.Request, response: express.Response) => {
    response.send('OK');
}
// (o)
app.get('/health', (request, response) => {
    response.send('OK');
}

추론 될 수 있는 경우라도 객체 리터럴과 함수 반환에는 타입 명시를 고려해야합니다. 이는 내부 구현의 오류가 사용자 코드 위치에 나타나는 것을 방지해줍니다.

객체 리터럴을 정의할 때 타입을 명시하면, 잉여속성 체크(아이템 11)가 동작합니다.

잉여속성 체크는 선택적 속성이 있는 타입의 오타 같은 오류를 잡는데 효과적이며 변수가 사용되는 시점이 아닌 할당하는 시점에 오류가 표시되도록 해줍니다.

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

// 타입 명시
const elmo: Product = {
  id: 12312312,
  name: 'Tickle Me Elmo',
  price: 23.58,
}

// 타입 구문 제거
const furby = {
  id: 12312312,
  name: 'Tickle Me Elmo',
  price: 23.58,
}

타입 구문을 제대로 명시한다면 실제로 오류가 발생한 부분에 오류 표시

타입 구문을 제거한다면 잉여 속성 체크가 동작하지 않고 객체를 선언한 곳이 아니라 사용되는 곳에서 타입 오류가 발생함

함수의 반환에도 타입을 명시하여 오류를 방지할 수 있습니다. 아래와 같은 이점이 있습니다.

구현상의 오류가 사용자 코드의 오류로 표시되지 않습니다

const cache: {[ticker: string]: number} = {}
function getQuote(ticker: string) {
    if(ticker in cache {
        //error : 반환타입이 Promise.resolve(cache[ticker])가 되어야 함
        return cache[ticker];
    }

    return fetch(`https://quotes.example.com/?q=${ticker}`)
    .then(respone => respone.json())
    .then(quote => {
        cache[ticker] = quote;
        return quote;
    })
}

오류가 함수를 호출한 코드에서 발생

const cache: {[ticker: string]: number} = {}
function getQuote(ticker: string): Promise<number>{ //반환 타입 명시
// ...
}

정확한 위치에 오류 표시

타입을 명시하면 함수에 대해 더욱 명확하게 알 수 있습니다.

명명된 타입을 사용하기 위해서

interface Vector2D {x: number, y: number};
function add(a:Verctor2D, b:Vector2D) {
    return {x: a.x + b.x , y: a.y + b.y}
}

// 반환 타입을 Vector2D이 아닌 {x: number, y: number}으로 추론

linter를 사용하고 있다면 eslint 규칙 중 no-inferrable-types을 사용해서 작성된 모든 타입 구문이 정말로 필요한지 확인할 수 있습니다.

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

변수의 값은 바뀔 수 있지만 타입은 일반적으로 바뀌지 않습니다.

// js에서는 문제 없지만 ts에서는 오류가 생기는 코드
let id = '12-34-56' // string 타입으로 추론했기 때문
id = 123456 //number를 할당할 수 없음

타입을 바꿀 수 있는 방법은 범위를 좁히는 것인데 확장이 아니라 타입을 더 작게 제한하는 것입니다.

(이 관점에 반하는 타입 지정 방법도 있지만 예외이지 규칙이 아님)

혼란을 막기 위해 타입이 다른 값을 다룰 때에는 변수를 재사용하지 않도록 합니다.

// 별도의 변수 사용하여 fix
const id = '12-34-56'
const serial = 12345

// 재사용 변수와 가려지는(shadowed) 변수와 혼동하지 않도록 합시다
const id = '12-34-56'
fetchProduct() {
    const id = 123456;
    fetchProductBySerialNumber(id)

}

item 21: 타입 넓히기

타입스크립트가 넓히기를 통해 상수의 타입을 추론하는 법을 이해해야합니다.

모든 변수는 런타임에 유일한 값을 가지지만, 타입스크립트가 작성된 코드를 체크하는 정적 분석 시점에 변수는 가능한 값들의 집합인 타입을 가집니다.

상수를 사용하여 변수를 초기화할 때 타입을 명시하지않으면 타입 체커는 타입을 결정해야합니다. 지정된 단일한 값을 가지고 할당 가능한 값들의 집합을 유추해야한다는 뜻입니다. 이러한 과정을 넓히기(widening) 라고 부릅니다.

interface Verctor3 {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' 형식의 매개변수에 할당될 수 없습니다.

x의 타입은 할당 시점에 넓히기가 동작해서 string으로 추론되었기 때문에 getComponent가 매개변수에 기대하는 타입인 'x' | 'y' | 'z'에 할당이 불가능하므로 오류가 발생합니다.

타입 넓히기는 주어진 값으로 추론 가능한 타입이 여러 개라면 타입스크립트는 작성자의 의도를 추측합니다. 위의 예제에서 타입스크립트는 다음 과 같은 코드를 예상했기 때문에 x의 타입을 string으로 추론했습니.

let x ='x'
x = 'a'
x = 'Four scroe and seven years ago..'

동작에 영향을 줄 수 있는 방법인 const, 타입 구문, 문맥, as const에 익숙해져야합니다.

타입스크립트는 넓히기의 과정을 제어할 수 있도록 몇 가지 방법을 제공합니다.

let 대신 const로 변수를 선언하기

const는 재할당될 수 없으므로 타입스크립트는 의심 없이 더 좁은 타입으로 추론할 수 있습니다.

const x = 'x'        //type이 'x'
let vec = {x: 10, y: 20, z: 30}
getComponent(vec, x) // 정상 : 'x'는 'x' | 'y' | 'z'에 할당 가능

const를 사용해도 객체나 배열에서는 여전히 문제가 있습니다.

// 아래 코드는 js에서 정상입니다.
const v  = {
    x: 1,
}

v.x = 3;
v.x = '3';    // Type 'string' is not assignable to type 'number'
v.y = '4';    // Property 'y' does not exist on type '{ x: number; }'.
v.name = 'pythagoras'
// Property 'name' does not exist on type '{ x: number; }'

v의 타입 후보

  • 가장 구체적 : {readonly x : 1}
  • 조금 추상적 : {x: nubmer}
  • 가장 추상적: {[key:string]: number} 혹은 object

타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다루기 때문에 v의 타입은 {x: nubmer}가 됩니다. 그렇기때문에 string을 할당할 수 없고, 다른 속성을 추가하지도 못합니다.

타입스크립트는 잘못된 추론을 할 정도로 구체적으로 타입을 추론하지는 않습니다. 타입 추론의 강도를 직접 제어하려면 타입스크립트의 기본 동작을 재정의해야합니다.

명시적 타입 구문을 제공

v: {x: 1|3|5} = {x: 1}

타입 체커에 추가적인 문맥을 제공하기

e.g. 함수의 매개변수로 값을 전달

const 단언문을 사용하기

값 뒤에 as 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;}

item 22: 타입 좁히기

분기문 외에도 여러 종류의 제어 흐름을 살펴보며 타입스크립트가 타입을 좁히는 과정도 이해해야합니다.

타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말합니다.

일반적인 예시로는 null 체크가 있습니다.

const el = document.getElementById('foo') // el의 타입: HTMLElement | null
if (el) {
    el.innerHTML = 'PartyTime'            // el의 타입 : HTMLElement
} else {
    alert('No element #foo')              // el의 타입 : null
}

첫번째 블록에서 null를 제외하므로 더 좁은 타입이 되어 작업이 수월해집니다.

만약 타입 별칭이 존재한다면 좁히기가 제대로 되지 않을 수도 있습니다. (아이템 24)

이 외에 다양한 방식으로 타입을 좁힐 수 있습니다.

  • 분기문에서 예외를 던지거나 함수를 반환
  • instaceof
  • 속성 체크
  • 내장 함수

조건문에서 타입을 좁힐 때 섣불리 판단하는 실수를 저지르지 맙시다.

function foo(x?: number|string|null)
if(!x) {
    x; // 타입이 string | number | null | undefined
}
// ''과 0 모두 false가 되기 때문

태그된/구별된 유니온과 사용자 정의 타입 가드를 사용하여 타입 좁히기 과정을 원할하게 만들 수 있습니다.

타입을 좁히는 또 다른 일반적인 방법은 명시적 태그를 붙이는 것입니다. 이 패턴은 태그된 유니온 또는 구별된 유니온이라고 불립니다.

interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string;}

type AppEvent = UploadEvent | DownloadEvent

function handleEvent(e: AppEvent) {
    switch (e.type) {
        case 'download':
            e           // e: DownloadEvent
            break
        case 'upload':
            e           // e: UploadEvent
            break
    }
}

만약 타입스크립트가 타입을 식별하지 못한다면, 식별을 돕기 위해 커스텀 함수를 도입할 수 있습니다.

function isInputElement(el: HTMLElement): el is HTMLInputElement {
    return 'value' in el
}

function getElementCotent(el: HTMLElement) {
    if (isInputElement(el)) {
        el              //  el : HTMLInputElement
        return el.value
    }
    el                  // el : HTMLElement
    return el.textContent
}

이러한 기법을 사용자 정의 타입가드라고 합니다. el is HTMLInputElement는 함수의 반환이 true인 경우 타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려줍니다.

어떤 함수들은 타입 가드를 사용하여 배열과 객체의 타입 좁히기를 할 수 있습니다.

 

반응형

'Web > TypeScript' 카테고리의 다른 글

[TypeScript] 제네릭  (0) 2022.08.25
[TypeScript] 고급타입  (0) 2022.08.24
[TypeScript] 인터페이스  (0) 2022.08.19
[TypeScript] 클래스  (0) 2022.08.17
[TypeScript] TypeScript 컴파일러  (0) 2022.08.12
Comments