[독서] 이펙티브 타입스크립트 아이템 16 - 20

dante Yoon·2021년 9월 5일
0
post-thumbnail

독서 후 개인적으로 정리한 내용을 기록한 글입니다.

아이템 16 number 인덱스 시그니처보다는 Array, 튜플, ArrayLike을 사용하기

type coersion은 암시적 타입 강제라고 번역되며 자바스크립트에서 가장 이상한 부분 중에 하나다.

"0" == 0
// true

자바스크립트에서 객체란 키/값 쌍의 모음이다. 키는 문자열만 가능하다.
배열에서는 숫자 인덱스로 참조해도 런타임에 문자열로 자동 변환된다.
만약 키가 number만 사용되기 원한다면 이에 맞는 배열 타입을 정의할 수 있다.

interface Array<T> {
  // ...
  [n: number]: T;
}

const x1: Array<any> = someArray["1"] // 타입 에러

Object.keys와 같은 구문은 여전히 문자열로 반환된다.
위의 예제 코드와는 다르게 for in 문에서는 실용적인 허용이 가능하다.

const keys = Object.keys(x1); // 타입이 string[]
for ( const key in x1) {
  // key는 타입이 string
  const x = x1[key];

자바스크립트 엔진에서 for-in 루프는 for-of 또는 for 루프에 비해 몇 배나 느리므로 잘 판단해서 사용해야 한다.
인덱스 시그니처로 number 형식이 가능하다고 하더라도 실제로 런타임에서는 항상 string 타입으로 변환되기에 이 부분이 혼란을 가져다 줄 수 있다.
string 대신 number를 인덱스 타입으로 사용하면 특별한 의미가 있다는 오해를 불러일으킬 수 있다.

특정 길이를 가진 튜플을 사용하고 싶다면 ArrayLike을 사용할 수 있다.

const tupleLike: ArrayLike<string> = {
 "0": "A",
 "1": "B",
  length: 2,
}// 정상
function checkAccess<T>(xs: ArrayLike<T>, i: number): T {
  if(i < xs.length) {
    return xs[i];
  }
}

아이템 17 변경 관련된 오류 방지를 위해 readonly 사용하기

배열을 인자로 받아 연산하는 함수는 인자가 참조하는 배열의 원본을 바꿀 수 있기 때문에
사이드 이펙트를 원치 않는 함수에서는 readonly 접근 제어자를 사용할 수 있다.

아래 함수는 사이드 이펙트를 발생한다.
연산이 끝나면 arr 는 빈 배열이 되기 때문이다.

 function arraySum(arr: number[]) {
   let sum = 0, num;
   while((num = arr.pop()) !== undefined) {
     sum += num;
   }
   return sum;

접근 제어자를 사용해 개선해보자.

function arraySum(arr: readonly number[]){
  let sum = 0, num;
  while((num = arr.pop()) !== undefined) {// 타입 에러
    sum+=num;
  }
  return sum;
}

readonly number[]number 타입과 아래의 내용에서 구분된다.
배열의 요소를 읽을 수는 있지만 쓸 수는 없다.
length를 읽을 수는 있지만 바꿀 수는 없다.
배열을 변경하는 pop을 비롯한 다른 메서드를 호출 할 수 없다.

readonly number[]number 타입보다 기능이 적으므로 number[]readonly number[]의 서브 타입이다.

readonly를 선언하면
타입 스크립트가 매개변수가 함수내에서 변경이 일어나는지 체크하며,
호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게된다.

constreadonly의 차이점을 알아야 한다.
readonly는 얕게 동작한다.

아이템 18 매핑된 타입을 사용하여 값을 동기화 하기

리엑트에서는 props가 변경될 때 해당 컴포넌트와 자식 컴포넌트가 리렌더링되는데,
이벤트 핸들러 같이 눈에 보이는 요소가 아니라면 해당 부분이 바뀌어도 다시 렌더링 될 필요가 없을 수 있다.

interface ViewProps {
  // data
  xs: number[];
  ys: number[];
  // display
  xRange: [number,number];
  yRange: [number, number];
  color: string;
  // event
  onClick: (x:number, y: number, index: number) => void;
}

최적화를 위해 다시 리렌더링이 필요한지 판단하는 함수를 만들어 보자.
아래 함수는 어떤 props가 추가되더라도 해당 props에 대해서도 항상 판단하기 때문에
필요한 업데이트를 놓치는 에러를 허용하지 않는다.
이렇게 처리하는 것을 보수적 접근법이라고 하며 실패에 닫힌 접근법이라고 한다.

function shouldUpdate(
  oldProps: ViewProps,
  newProps: ViewProps,
){
  let k: keyof ViewProps;
  for (k in oldProps){
    if(oldProps[k] !== newProps[k]){
      if(k !== "onClick") return true;
    }
  }
}

실패에 열린 접근법으로 작성을 해보자. 특정 props에 한해서만 리렌더링을 허용하여 너무 자주 업데이트 되는 것을 막는다.
하지만 정작 필요할 때 뷰가 바뀌지 않을 수 있으므로 최적화라고 보긴 어렵다.

function shouldUpdate(
  oldProps: ViewProps,
  newProps: ViewProps,
){
  return(
    oldProps.xs !== newProps.xs ||
    oldProps.ys !== newProps.ys ||
    oldProps.xRange !== newProps.xRange ||
    oldProps.yRange !== newProps.yRange ||
    oldProps.color !== newProps.color
  )
}

타입체커가 위와 같은 역할들을 대신하도록 만들자.

const REQUIRES_UPDATE: {[k in keyof ViewProps]: boolean} = {
 xs: true,
 ys: true,
 xRange: true,
 yRange: true,
 color: true,
 onClick: false
 }

function shouldUpdate(
 oldProps: ViewProps,
 newProps: ViewProps,
 ) {
   let k: keyof ViewProps;
   for (k in oldProps){
     if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){
       return true;
     }
   }
 }

[k in keyof ViewProps]는 타입 체커에게 ViewProps와 동일한 속성을 가져야 한다는 것을 알려준다.
REQUIRES_UPDATE에 boolean 값을 가진 객체를 사용했다.
나중에 추가 속성이 더해질때 REQUIRES_UPDATE에서 에러가 발생할 것이다.

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

자바스크립트를 타입스크립트로 변환할때 많은 타입 구문을 추가해야겠다는 생각이 들지만 사실 모든 변수에 타입을 선언하는 것은 비 생산적이다. 타입 추론이 된다면 명시적 타입 구문은 필요하지 않기 때문이다.

let x:number = 12;
let x = 12; 

더 복잡한 객체도 추론이 가능하다.

const person = {
  name: "DanteYoon",
  born: {
    where: "Seoul",
    when: "long time ago",
  },
}

배열의 경우도 동일하다. 타입스크립트는 입력받아 연산을 하는 함수가 어떤 타입을 반환하는지 추론할 수 있다.

function square(nums: number[]){
  return nums.map(x =>x *x);
}
const squares = square([1,2,3]) // number[];

비구조화 할당문도 동일하게 지역변수의 타입이 추론된다.

정보가 부족해서 타입스크립트가 타입을 판단하기 어려운 상황이 발생하기도 한다.
함수의 매개변수 타입이 그러하다.
라이브러리를 사용할 때 타입 정보가 기본적으로 제공된다면 콜백함수 매개변수 타입은 자동으로 추론된다. 아래 express를 사용하는 예제에서 request, response에 명시적 타입을 선언하지 않는다.

app.get("/api", (request,response) => {
  response.send("ok");
});

타입을 명시해야하는 상황이 있다. 잉여 속성 체크가 필요할 때이다.

const elmo:Product = {
  name: "something",
  id: "123123",
  price: 1000,
}

추론이 가능하지만 함수의 반환에 타입을 명시한다면, 구현상의 오류가 함수를 호출한 곳까지 영향을 미치는 것을 막을 수 있다.

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

자바스크립트에서는 한 변수에 다른 타입의 값을 선언해서 재사용해도 된다.
타입스크립트에서는 오류가 발생한다. id 12-12-12를 선언할 때 이미 string으로 추론이 되었기 때문이다. 변수의 값은 바뀔 수 있으나 타입이 바뀌지는 않는다.

let id = "12-12-12";
id = 121212; // 타입 에러

타입을 바꿀 수 있게 하기 위해서는 타입을 더 작게 제한하는 것이다.

let id: string | number = "12-12-12";
id = 123456 // 정상

유니온 타입으로 타입체커 오류를 해결한다 하더라도 id를 사용할 때마다 어떤 타입인지 확인해야 하기 때문에 별도의 변수를 도입하는 것이 더 낫다.
또한 const 키워드를 사용할 수 있기 때문에 꼭 재할당이 필요한지 잘 판단하자.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글