[이펙티브 타입스크립트] 3장 타입 추론

JIY00N·2023년 8월 6일
0

TypeScript

목록 보기
4/9
post-thumbnail
post-custom-banner

2023.08.07~08.10 아이템19~27

3장 타입 추론

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


  • 타입스크립트의 많은 타입 구문은 사실 불필요하다.
  • 오히려 타입스크립트가 더 정확하게 추론한다.
const axis1: string = 'x'; // 타입은 string
const axis2 = 'y'; // 타입은  "y"
  • 비구조화 할당문(구조 분해 할당)으로 모든 지역 변수의 타입이 추론된다.
interface Product{
  id: string;
  name: string;
  price: number;
}
// 비구조화 할당문
function logProduct(product: Product){
  const {id, name, price} = product;
  console.log(id, name, price);
}
  • 타입스크립트에서 변수의 타입은 처음 등장 할 때 결정된다.

  • 함수 매개변수에 타입 구문을 생략하는 경우

1. 기본 값이 있는 경우

// base = 10으로 초기화 -> 타입은 number로 추론
function paraseNumber(str: string, base = 10){...}

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

// express HTTP 서버 라이브러리를 사용하는 request와 response의 타입 선언은 불필요
// bad
app.get('/health', (request: express.Request, response: express.Response) => {
  response.send('ok');
})
        
// good
app.get('/health', (request, response) => {
  response.send('ok');
})
  • 타입 추론이 가능하지만, 명시하고 싶은 경우

1. 객체 리터럴을 정의할 때

엄격한 객체 리터럴 체크(잉여속성체크)가 동작함
-> 선택적 속성이 있는 타입의 오타를 잘 잡는다.
-> 할당하는 시점(실제로 실수가 발생한 부분)에 오류를 표시

2. 함수의 반환에 타입을 명시하자

  1. 오류의 위치를 정확히 표시해준다.
  2. 반환 타입을 명시하면 함수에 대해 더욱 명확하게 알 수 있다.
  3. 명명된 타입을 사용하기 위해서다.
// add의 매개변수처럼 함수의 반환에도 타입을 입력해주자
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;}로 추론했다.
// add함수의 매개변수는 명명 타입을 가지지만, 추론된 반환타입은 그렇지 않다.
  • eslint 규칙 중 no-inferrable-types: 작성된 모든 타입 구문에 정말로 필요한지 확인할 수 있다.

🎯 요약

타입스크립트의 타입 구문을 필요시에 적절히 잘 사용해야한다.

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


  • 변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다.

  • 다른 타입에는 유니온 대신 별도의 변수를 사용하자

    1. 서로 관련이 없는 두 개의 값을 분리한다.
    2. 변수명을 더 구체적으로 지을 수 있다.
    3. 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
    4. 타입이 좀 더 간결해진다. (string|number) -> string 또는 number
    5. let대신 const로 변수를 선언하게 된다.
  • 변수의 유효범위를 주의하자(가려지는 변수)

const id = '123'
fetchProduct(id);
{
  const id = 123; // 정상
  fetchProduct(id); // 정상
}

🎯 요약

타입이 다른 값을 사용할 때는 변수를 재사용하지 말자

아이템 21 타입 넓히기


  • 타입스크립트가 코드를 체크하는 시점(정적 분석 시점)에 변수는 '가능한' 값들의 집합인 타입을 가진다. (= 넓히기)
    -> 타입 넓히기가 진행되면, 주어진 값으로 추론 가능한 타입이 여러 개 이기 때문에 모호하다.

  • 넓히기 제어하기

1. 객체 와 배열을 제외하고, const 사용

interface Vector3{x: number; y: number; z: number;}
function getComponent(vector: Vector3d, axis: 'x' | 'y' | 'z'){
  return vector[axis];
}
// bad
let x = 'x'; 
let vec = {x: 10, y: 20, z: 30};
getComponent(vec,x); // 오류 
// -> 'string' 형식의 인수는 'x' | 'y' | 'z' 형식의 매개변수에 할당될 수 없다.

// good
// x는 재할당 불가
const x = 'x'; 
let vec = {x: 10, y: 20, z: 30};
getComponent(vec,x); // 정상 , x: 'x'

2. 객체의 const

2-1 객체의 const에서 타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다룬다.

const v = {
  x: 1,
}; // v의 타입은 {x: number}
v.x = 3; // 정상
v.x = '3'; // '"3"' 형식은 'number' 형식에 할당할 수 없습니다.
v.y = 4; // '{x: number;}' 형식에 'y' 속성이 없습니다.
v.name = 'lee'; // '{x: numer;}' 형식에 'name' 속성이 없습니다.

2-2 타입스크립트의 기본 동작을 재정의 하여 타입 추론의 강도를 직접 제어하자.

a. 명시적 타입 구문을 제공

const v: {x: 1|3|5} = {
  x: 1,
}; // 타입이 {x: 1|3|5;}

b. 타입 체커에 추가적인 문맥을 제공

예) 함수의 매개변수로 값을 전달

c. const 단언문 사용

  1. 변수 constconst 단언문(as const)은 다르다.
    -> as const 는 최대한 좁은 타입으로 추론
// 1. 변수 const
const v1 = {
  x:1,
  y:2,
}; // 타입은 {x: number; y: number;}

// 2. 속성 값 뒤에 const 단언
const v2 = {
  x: 1 as const,
  y: 2,
} // 타입은 {x: 1; y: number;}

// 3. 객체 const 단언
const v3 = {
  x:1,
  y:2,
} as const; // 타입은 {readonly x: 1; readonly y: 2;}
  1. 배열을 튜폴로 추론할 수 있다.
const arrayA = [1,2,3]; // 타입이 number[]
const tuplaeA = [1,2,3] as const; // 타입이 readonly [1,2,3]

🎯 요약

타입 넓히기를 이해하고, 제어하는 방법을 알자

아이템 22 타입 좁히기


  • 타입 좁히기는 넓은 타입으로부터 좁은 타입으로 진행하는 과정이다.
  • 타입을 좁힐 수 있는 방법들

조건문, instanceof, 속성체크, Array.isArray, 명시적 태그 붙이기(태그된 유니온, 구별된 유니온, (고유 타입)), 타입 가드(is)...

// 1. 조건문에서의 null 체크 
// -> 단, typeof null 체크는 "object"이므로 제대로 좁혀지지 않을 수 있다.
// good
const el = document.getElementById('foo'); // 타입이 HTMLElement | null
if(el){
  el // 타입이 HTMLElement
  el.innerHTML = 'party time'.blink(); //.blink(): 텍스트를 깜빡이게 만드는 HTML요소메서드
}else{
  el // 타입이 null
  alert('No element #foo');
}

// bad
const el = document.getElementById('foo'); // 타입이 HTMLElement | null
if(typeof el === 'object'){
  el // 타입이 HTMLElement | null
}

// 2. instanceof
function contains(text: string, serach: string|RegExp){
  if(search instanceof RegExp){
    search // 타입이 RegExp
    return !!search.exec(text); 
    // search.exec(text)의 반환값이 null이 아니라면, 
    // 즉 일치하는 부분이 있을 경우에는 true로 변환
    // 일치하는 부분이 없으면(null을 반환하면) false로 변환 
  }else{
    search // 타입이 string
    return text.includes(search);
  }
}

// 3. 속성체크
interface A {a: number}
interface B {b: number}
function pickAB(ab: A|B){
  if('a' in ab){
    ab // 타입이 AB
  }else{
    ab // 타입이 B
  }
  ab // 타입이 A|B
}

// 4. Array.isArray
function contains(text: string, terms: string|string[]){
  const termList = Array.isArray(terms) ? terms : [terms];
  termList // 타입이 string[]
  // ...
}

// 5. 명시적 태그(태그된 유니온, 구별된 유니온)
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 // 타입이 DownloadEvent
      break;
    case 'upload':
      e // 타입이 UploadEvent
      break;
  }
}

// 6. 타입 가드(is)
// bad -> undefined가 걸러지지 않았다.
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marion', 'Michael'];
const members = ['Janet', 'Michael']
  .map(who => jackson5.find(n => n === who))
  .filter(who => who !== undefined); // 타입이 (string|undefined)[]

//good
function isDefined<T>(x: T | undefined): x is T{
  return x !== undefined;
}
const members = ['Janet', 'Michael']
  .map(who => jackson5.find(n => n === who))
  .filter(isDefined); // 타입이 string[]

🎯 요약

타입을 좁힐 수 있는 방법들을 알아두자

아이템 23 한꺼번에 객체 생성하기


  • 객체를 생성할 때는, 여러 속성을 한꺼번에 생성해야 타입 추론에 유리하다.
    (타입 단언문(as)로 각각 만들 수 있지만 별로다.)

  • 객체 전개 연산자(...)

// 1. 작은 객체들(pt, id)에서 큰 객체 만들기(namedPoint)
const pt = {x: 3, y: 4};
const id = {name: 'lee'};
const namedPoint = {...pt, ...id};
namedPoint.name; // 정상, 타입이 string

// 2. 타입 걱정 없이 필드 단위로 객체 생성
// 모든 업데이트마다 새 변수를 사용하여 각각 새로운 타입을 얻도록 해야 함
interface Point { x: number, y: number};
const pt0 = {}; 
const pt1 = {...pt0, x: 3}; // x: number
const pt: Point = {...pt1, y: 4}; // Point

// 3. 안전한 방식으로 조건부 속성 추가 -> null 또는 {} 사용
declare let hasMiddle: boolean;
const firstLast = {first: 'lee', last: 'yoon'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
/*
const president: {
    middle?: string | undefined;
    first: string;
    last: string;
}*/

// 4. 한꺼번에 여러 속성 추가
declare let hasDates: boolean;
const nameTitle = {name: 'lee', title: 'hi'};
const pharaoh = {
  ...nameTitle,
  ...(hasDates ? {start: -2589, end: -2566} : {})
};
pharaoh.start 
// 오류 -> '{name: string; title: string;}' 형식에 'start' 속성이 없습니다.

// pharaoh.start를 사용하고 싶다면 헬퍼 함수 사용
function addOptional<T extends object, U extends object>(
  a: T, b: U | null): T & Partial<U>{
  return {...a, ...b};
}

const pharaoh = addOptional(
  nameTitle,
  hasDates ? {start: -2589, end: -2566} : null);
pharaoh.start // 정상, 타입이 number | undefined;
  • 객체나 배열을 변환해서 새로운 객체나 배열을 만들고 싶을 때, 내장된 함수형 기법 또는 로대시(Lodash) 사용

🎯 요약

객체를 한꺼번에 만들자, 안전한 타입으로 속성을 추가하려면 객체 전개 연산자를 사용하자(...)

아이템 24 일관성 있는 별칭 사용하기


  • 별칭의 값을 변경하면 원래 속성값에서도 변경된다.
    -> 별칭을 신중하게 사용해야 한다.
const borough = {name: 'Brooklyn', location: [40.688, -73.979]};
const loc = borough.location; // borough.location에 loc 별칭 사용

loc[0] = 0;
borough.location; // [0, -73.979] 원래 속성도 0으로 변경 됨

1. 별칭은 일관성 있게 사용하자.
2. 객체 비구조화(구조 분해)를 이용하자.

// 별칭 일관되게 사용, 객체 비구조화(구조 분해) 사용
function isPointInPolygon(polygon: Polygon, pt: Coordinate){
  const {bbox} = polygon;
  if(bbox){
    const {x, y} = bbox;
    if(pt.x < x[0] || pt.x > x[1] ||
       pt.y < y[0] || pt.y > y[1]){
      return false;
    }
  }
  //...
}     

구조 분해 사용시 주의점
2-1. 선택적 속싱일 경우, 타입의 경계(?)에 null 값을 추가하는 것이 좋다.
2-2. (?) 빈 배열은 'holes 없음'을 나타내는 좋은 방법이다.

3. 속성보다 지역변수를 사용하자.
-> 함수 호출이 객체 속성의 타입 정제를 무효화 할 수 있다.

function fn(p: Polygon){//...}
polygon.bbox; // 타입이 BoundingBox | undefined
if(polygon.bbox){
  polyhon.bbox; // 타입이 BoundingBox
  fn(polygon);
  polyhon.bbox; // 타입이 BoundingBox
}

🎯 요약

타입 별칭을 사용할 때는, 신중하게

아이템 25 비동기 코드에는 콜백 대신 async 함수 사용하기


  • 비동기 동작 모델링
    ES2015 이전: 콜백 사용
    ES2015: 프로미스(Promise) 사용
    ES2017: async & await 사용

  • 콜백보다 프로미스나 async & await를 사용하자.

  1. 콜백보다는 프로미스가 코드를 작성하기 쉽다.
  2. 콜백보다는 프로미스가 타입을 추론하기 쉽다.
  • 프로미스보다 async & await를 사용하자.
  1. 일반적으로 더 간결하고 직관적인 코드가 된다.
  2. async 함수는 항상 프로미스를 반환하도록 강제한다.

🎯 요약

비동기 함수에서는 async & await를 사용하자

아이템 26 타입 추론에 문맥이 어떻게 사용되는지 이해하기


  • 타입스크립트는 타입을 추론할 때 값과 값이 존재하는 곳의 문맥(코드의 위치)까지 살핀다.

  • 문맥과 값을 분리했을 때 변수, 튜플, 객체, 콜백에서 생기는 문제점들

1. 변수 사용 시

타입 선언 또는 const 키워드 사용

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language){ }
// 1. 인라인 형태 
// ts가 -> 함수 선언을 통해 매개변수가 Language 타입이어야 한다는 것을 알음
setLanguage('JavaScript'); // 정상

// 2. 참조 형태
// ts가 -> 할당 시점에 타입을 추론함 -> string으로 추론
let language = 'JavaScript';
setLanguage(language);
// 'string' 형식의 인수는 'Language' 형식의 매개변수에 할당될 수 없습니다.

// 해결법1. 타입선언에서 language의 가능한 값을 제한하는 것
let language: Language = 'JavaScript';
setLanguage(language); // 정상

// 해결법2. language를 const를 사용해 상수로 만든다.
const language = 'JavaScript';
setLanguage(language); // 정상

2. 튜플 사용 시

function panTo(where: [number, number]){}
// 1. 인라인 형태 
panTo([10,20]); // 정상

// 2. 참조 형태
const loc = [10,20];
panTo(loc); 
// 'number[]' 형식의 인수는 '[number, number]' 형식의 매개변수에 할당 할 수 없다.

// 해결책 -> panTo 함수에 readonly 구문을 추가하고, as const 사용
function panTo(where: readonly [number, number]){}
const loc = [10,20] as const;
panTo(loc); // 정상
// 다만, 타입 정의에 실수가 있으면(ex. 튜플에 값 추가)
// 근본적인 오류가 어디서 났는지 알기 힘들다.

3. 객체 사용 시

타입선언(const a: b = ...)을 추가하거나 상수 단언(as const)를 사용한다.

4. 콜백 사용 시

매개변수에 타입 구문을 추가 하거나 전체 함수 표현식에 타입 선언을 적용

🎯 요약

타입 추론에 문맥이 어떻게 사용되는지 이해하여 대처하자

아이템 27 함수형 기법과 라이브러리로 타입 흐름 유지하기


  • 타입 흐름을 개선하고, 가독성을 톺이고, 명시적인 타입 구문의 필요성을 줄이자.

  • 이는 직접 구현하기보다는 내장된 함수형 기법과 로대시 같은 유틸리티 라이브러리를 사용하자.

  • Array.prototype.map(내장 함수)보다 _.map(로대시)를 사용하려는 이유는?

    콜백을 전달하는 대신 속성의 이름을 전달할 수 있다.

interface BasketballPlayer{
  name: string;
  team: string;
  salary: number;
}
declare const rosters: {[team: string]: BasketballPlayer[]};
const allPlayers = Object.values(rosters).flat();

const namesA = allPlayers.map(player => player.name); // 타입이 string[]
const namesB = _.map(allPlayers, player => player.name); // 타입이 string[]
const namesC = _.map(allPlayers, 'name'); // 타입이 string[]
const salaries = _.map(allPlayers, 'salary'); // 타입이 number[]
const teams = _.map(allPlayers, 'team'); // 타입이 string[]
const mix = _.map(allPlayers, Math.random() < 0.5 ? 'name' : 'salary');
// 타입이 (string|number)[]

🎯 요약

직접 구현보다는 내장된 함수형 기법과 로대시 같은 유틸리티 라이브러리를 사용하자

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글