[이펙티브 타입스크립트] - 아이템21 ~ 아이템30

Lee Jeong Min·2022년 3월 2일
1

TypeScript

목록 보기
13/18
post-thumbnail

[아이템21] 타입 넓히기

작성된 코드를 체크하는 정적 분석 시점에 변수는 ‘가능한' 값들의 집합인 타입을 가진다. 이 과정에서 타입을 명시하지 않았을 때, 타입 체커는 타입을 결정해야 한다. 이 말은 지정된 단일 값을 가지고 할당 가능한 집합을 유추해야하는데 이를 ‘넓히기’ 라고 한다.

interface Vector3 {
    x: number;
    y: number;
    z: number;
}
const getComponent = (vector: Vector3, axis: 'x' | 'y' | 'z') => {
    return vector[axis];
}

// x가 현재 string으로 추론되어 
let x = 'x';
let vec = {x:10, y:20, z: 30};
getComponent(vec, x); // 여기서 에러 발생!

이런 넓히기의 과정을 제어하는 방법

  • const 사용 (ex: const x = ‘x’// 타입이 ‘x’)
    → 그러나 객체와 배열의 경우 문제가 해결되지 않음.(타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다루기 때문)

따라서 이러한 부분을 해결하려면 타입 추론의 강도를 직접 제어해야 함.

타입 추론의 강도를 직접 제어하여 타입스크립트의 기본 동작을 재정의 하는 방법

  • 명시적 타입 구문 제공
const v: { x: 1|3|5 } = { x: 1 }; // 타입이 {x: 1|3|5; }
  • 타입 체커에 추가적인 문맥 제공 (함수의 매개변수로 값을 전달)
  • const 단언문 사용 → as const 사용

최근 벨로퍼트 redux를 보고 있는데 거기서도 이러한 문법을 사용(action 타입과 action 생성 함수를 정의하고 타입으로 넘겨줄때)

const SET_DIFF = 'counter/SET_DIFF' as const;
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;

export const setDiff = (diff: number) => ({ type: SET_DIFF, payload: diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

type CounterAction = ReturnType<typeof setDiff> | ReturnType<typeof increase> | ReturnType<typeof decrease>;

export default function counter(state: CounterState = initalState, action: CounterAction) {
   ...

[아이템22] 타입 좁히기

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

가장 일반적인 예시로 돔요소를 가져와 null인지 체크하는 과정

타입을 좁히는 방법

  • 조건문
  • instanceof
  • Array.isArray와 같은 내장 함수
  • 명시적 ‘태그' (태그된 유니온, 구별된 유니온)
  • 사용자 정의 타입 가드(반환에 is 구문을 넣어 타입을 좁힐 수 있음을 알려줌)
const isDefined = <T>(x: T | undefined): x is T => {
  return x !== undefined;
};

위와 같이 반환하는 쪽에 x is T는 반환이 true인 경우
타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려준다.

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

객체를 생성할 때는 속성을 하나씩 추가하기보다는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다. 그러나 객체를 반드시 각각 나누어서 만들어야한다면 다음의 방법을 사용할 수 있다.

  • 타입 단언(as)문
  • 객체 전개 연산자

객체 전개 연산자로 하나의 속성을 조건부 연산으로 추가하면 타입이 선택적 속성을 가진것으로 추론된다. 그러나 여러개의 속성을 추가하면 이는 유니온으로 추론된다. 이 경우 선택적 필드 방식으로 표현하려면 아래와 같이 헬퍼 함수를 사용하자.

// 헬퍼함수
const addOptional = <T extends object, U extends object>(a: T, b: U | null): T & Partial<U> => {
  return { ...a, ...b };
};

declare let hasDates: boolean;
const nameTitle = { name: 'Khufu', title: 'Pharaoh' };
const pharaoh = addOptional(nameTitle, hasDates ? { start: -2589, end: -2566 } : null);
pharaoh.start // 정상, 타입이 number | undefined

가끔 객체나 배열을 변환해서 새로운 객체나 배열을 생성하려는 경우 내장된 함수형 기법 또는 로대시 같은 유틸리티 라이브러리를 사용하는 것이 ‘한꺼번에 객체 생성하기' 관점에서 보면 옳다.

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

const borough = {name: 'Brooklyn', location: [40.688, -73.979]};
const loc = borough.location;

위와 같은 loc이 별칭이다.

별칭 사용시 주의할점

  1. 제어 흐름 분석을 방해하지말자. → 별칭을 일관성 있게 사용!
  2. 객체 비구조화를 이용하여 일관된 이름을 사용하자.
    1. 그러나 선택적 속성인 경우 속성 체크가 필요하며 경계에 null값을 추가하는 것이 좋다.
    2. 빈 배열인 경우 그 값이 없음을 나타내는 좋은 방법으로 사용될 수 있다. → 이 부분 이해안가서 스터디때 러버덕

객체 속성에서 타입스크립트 제어 흐름 분석을 주의해야 한다.

function fn(p: Polygon) { /* ... */ }

polygon.bbox // 타입이 BoundingBox | undefined
if (polygon.bbox) {
  polygon.bbox // 타입이 BoundingBox
  fn(polygon);
  polygon.bbox // 타입이 BoundingBox
}

이 예제에서 fn함수가 polygon의 속성을 직접 수정한다면 그 이후의 타입이 달라질 가능성이 있다. 따라서 지역변수(bbox)로 비구조화 할당으로 꺼내서 사용하면 타입을 정확하게 유지할 수 있다. 그러나 이경우도 polygon.bbox의 값과 같게 유지되지 않을 수 있다.

→ 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 주의하자!

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

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

  • 콜백보다 프로미스가 코드 작성하기 쉽다.
  • 콜백보다 프로미스가 타입을 추론하기 쉽다.

Promise.all의 경우 await와 구조 분해 할당이 찰떡궁합이다.

const fetchPages = async () => {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1),
    fetch(url2),
    fetch(url3),
  ]);
  ...
};

또한 프로미스를 사용하면 타입 구문 없이 모든 타입 추론이 제대로 동작한다.(Promise<Response>)

프로미스 생성 시 async/await 사용의 이점

  • 일반적으로 더 간결하고 직관적인 코드가 된다.
  • async함수는 항상 프로미스를 반환하도록 강제된다.(실수로 함수안에 동기와 비동기를 섞어 쓰는 것을 방지할 수 있음)

따라서 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋다.

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

타입스크립트는 타입을 추론할때 값이 존재하는 곳의 문맥까지도 살핀다.

그러나 값을 변수로 분리한 경우

type Language = 'JS' | 'TS' | 'Python';
const setLanguage = (language: Language) => {}

let language = 'JS';
setLanguage(language);

이렇게 되면 할당시점에 타입을 추론하므로 string 으로 추론하게 된다.

이를 해결하는 2가지 방법

  • language 변수에 타입 구문을 넣어 가능한 값 제한 → let language: Language = ‘JS’
  • language 를 상수로 만드는 방법 → const language = ‘JS’

문맥과 값을 분리한 경우 발생할 수 있는 문제를 해결하는 방법

튜플

  • 타입 선언 제공
  • as const로 상수 문맥을 제공하며 전달되는 인수부분에 readonly 타입 추가

객체

  • 타입 선언 제공
  • 상수 단언 사용

콜백

  • 전체 함수 표현식에 타입 선언

상수 단언 사용 시 타입 정의에 실수가 있다면 오류는 타입 정의가 아닌 호출되는 곳에서 오류가 발생하므로 주의해야한다.

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

함수형 기법들을 TS와 함께 사용하면 타입 정보가 그대로 유지되면서 타입 흐름이 계속 전달되므로 좋다.

순수 JS로 절차형, 함수형보다 Lodash같은 유틸리티 라이브러리를 사용하면 타입 구문없이 사용가능한 경우가 많다. (ex: Lodash의 Dictionary 타입)

순수 map 대신 _.map을 사용하는 이유로 콜백을 전달하는 대신 속성의 이름을 전달할 수 있기 때문

타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기보다는 내장된 함수형 기법과 Lodash 같은 유틸리티 라이브러리를 사용하자.

4장 타입 설계

[아이템28] 유효한 상태만 표현하는 타입을 지향하기

효과적인 타입 설계를 위해선, 유효한 상태만 표현할 수 있는 타입을 만들어 내는 것이 중요하다.

// 이렇게만 상태를 나누고 이에따른 함수를 작성하기보다.
interface State {
  pageText: string;
  isLoading: boolean;
  error?: string;
}

// 이렇게 무효한 상태를 허용하지 않도록 코드가 길어지더라도 명시적으로 모델링하는 것이 좋다.
// 그 이후 이에따른 함수 작성
interface RequestPending {
    state: 'pending';
}

interface RequestError {
    state: 'error';
    error: string;
}

interface RequestSuccess {
    state: 'ok';
    pageText: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: { [page: string]: RequestState };
}

따라서 어떤 값들을 포함하고 어떤 값들을 제외할지 신중하게 생각하고, 유효한 상태만 표현하는 타입을 지향하자!

[아이템29] 사용할 때는 너그럽게, 생성할 때는 엄격하게

포스텔의 법칙

포스텔의 법칙(Postel's law)

견고함의 원칙 - 위키백과, 우리 모두의 백과사전

함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과 반환시 타입의 범위가 구체적이어야 한다.

이를 고려하기 위해 매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다.

[아이템30] 문서에 타입 정보를 쓰지 않기

  • 주석과 변수명에 타입 정보를 적는 것은 피하자.(되도록이면 코드로 표현, 주석은 누가 바꾸지 않는 이상 동기화 되지 않지만 타입 구문은 타입 정보를 동기화하도록 강제한다.)
  • 타입이 명확하지 않은 경우 변수명에 단위 정보를 포함하는 것을 고려하는 것이 좋다.
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글