타입스크립트에서 코드 안정성과 가독성을 높이는 법

젼이·2024년 2월 22일
post-thumbnail

[ITEM 17] 변경 관련된 오류 방지를 위해 readonly 사용하기

자바스크립트 배열은 내용을 변경할 수 있기 때문에,
타입스크립트에서도 역시 오류 없이 통과하게 된다.

👀 오류 범위를 좁히기 위해서 readonly 접근 제어자를 사용할 수 있다.

readonly 접근 제어자를 사용한 간단한 함수를 작성해봤다.

readonly가 뭐길래...? 메서드를 사용할 수 없는 이유가 뭘까?

오류 메시지는 아래와 같다.

Property 'pop' does not exist on type 'readonly number[]'.
'readonly number[]' 형식에 'pop' 속성이 없습니다.

이 오류 메세지를 자세히 살펴보자.

readonly number[] 는 타입이고 number[]와 구분되는 몇 가지 특징이 있다.

  • 배열의 요소를 읽을 수 있지만, 쓸 수는 없다.
  • length를 읽을 수 있지만, 바꿀 수는 없다.
  • 배열을 변경하는 pop을 비롯한 다른 메서드를 호출할 수 없다. (map이나 filter같은 원본을 수정하지 않고 새 배열을 반환하는 메서드는 사용이 가능!)

number[]readonly number[]보다 기능이 많이 때문에, readonly number[]의 서브 타입이 된다.

따라서, 변경 가능한 배열을 readonly배열에 할당할 수 있다. 하지만 그 반대는 불가능하다.

아래 코드를 보면 그 뜻이 좀 더 이해갈 것이다.

The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'

'readonly number[]' 타입은 'readonly'이므로 변경가능한 'number[]'타입에 할당 될 수 없습니다.

매개변수를 readonly로 선언하면,

  • 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다.
  • 호출하는 쪽에서 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다.
  • 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다.

자바스크립트에서는(타입스크립트도 마찬가지) 명시적으로 언급하지 않는 한, 함수가 매개변수를 변경하지 않는다고 가정한다.
그러나 이러한 암묵적인 방법은 타입 체크에 문제를 일으킬 수 있다.

const와 readonly의 차이

공통점 : const와 readonly는 초기 때 할당된 값을 변경할 수 있다.

Const

변수 참조를 위한 것이다.
변수에 다른 값을 할당할 수 없다.

readonly

속성을 위한 것이다.


testB.testC에 새로운 값이 할당될 수 있는 이유는 readonly가 얕게 동작하는 것과 관련이 있다.

참조 (refer)하고 있는 값 자체는 변경될 수 없지만 안에 있는 속성들이 모두 동일한 접근 제어자를 가지고 있는 것이 아니다.

배열에 readonly가 존재한다고 배열 자체가 readonly가 되지 않는다.


[ITEM 18] 매핑된 타입을 사용하여 값을 동기화하기

interface ScatterProps {
  // data
  xs: number[];
  xs: number[];

  // display
  xRange: [number, number];
  yRange: [number, number];
  color: string;

  // events
  onClick: (x: number, y: number, index: number) => void;
}

const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} { 
  // 매핑된 타입으로 ScatterProps 가 변경되면 REQUIRES_UPDATE 도 변경됨을 알림
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
};

function shouldUpdate(
  oldProps: ScatterProps,
  newProps: ScatterProps
) { 
  let k: keyof ScatterProps;
  for(k in oldProps) {
    if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) return true;
    return false;
  }
}
  • 매핑된 타입을 사용해서 관련된 값과 타입을 동기화하도록 해야한다.
  • 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려 해야한다.

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

타입스크립트의 많은 타입 구문은 사실 불필요하다. 코드의 모든 변수에 타입을 선언하는 것은 비생산적이며 형편없는 스타일이다.

타입 구문이 필요한 경우

  • 함수/ 메서드 시그니처

타입 구문이 필요하지 않은 경우

  • 함수 내의 지역 변수
  • 매개변수 기본값이 있는 경우
  • 타입 정보가 있는 라이브러리의 콜백함수 매개변수 타입 (ex. express)

타입이 추론되지만 타입을 명시하고 싶은 경우

객체 리터럴을 정의할 때

객체 리터럴에 타입을 명시하면 잉여 속성 체크에 의해서 타입 오타같은 오류를 잡을 수 있다. 그리고 변수가 사용되는 순간이 아니라 할당하는 시점에서 오류를 표시해준다.

만약 타입 구문을 제거하면 잉여 속성 체크가 동작하지 않고, 객체가 사용되는 곳에서 타입 오류가 발생한다.

함수의 반환에 타입을 명시

타입 추론이 가능할지라도 구현상의 오류가 함수를 호출한 곳까지 영향을 미칠 수 있기 때문에 타입 구문을 명시하는 것이 좋다.

반환 타입을 명시하면 함수에 대해 더욱 명확하게 알 수 있다.
반환값에 명명된 타입을 사용할 수 있다. (number 처럼 두루뭉실한 타입이 아니라 구체적인 타입을 명시할 수 있음)

반환 타입에 대한 주석을 작성할 수 있어서 더욱 자세한 설명이 가능하다.

eslint 규칙중 no-inferrable-types 를 사용해 작성된 모든 타입 구문이 정말 필요한 것인지 확인할 수 있다.


[ITEM 20] 다른 타입에는 별도의 변수 사용하기

  • 변수의 값은 바뀔 수 있지만 그 타입은 바뀌지 않아야 한다.
    서로 관련이 없는 두 개의 값을 분리한다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 더 간결해진다. (string|number 대신 string과 number 사용)
  • let 대신 const 로 변수를 선언하게 된다.

[ITEM 21] 타입 넓히기

변수의 범위 내에서 값이 변경될 때 타입 시스템의 유연성에 관련된 것이라고 볼 수 있다.

getComponent 함수는 두 번째 매개 변수에 "x" | "y" | "Z" 타입을 기대했지만, x 타입은 할당 시점에 넓히기가 동작해서 string으로 추론 되었다. string 타입은 "x"|"y"|"z" 타입에 할당이 불가능하므로 오류가 된 것이다.

타입 추론 강도를 제어하는 방법

1. const 키워드

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

const v: { x: 1|3|5; } = {
 x: 1, 
}; 

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

→ 함수의 매개변수로 값을 전달하는 경우

4. const 단언문을 사용

변수 선언이 아니라 const 단언이다.
const 단언문은 온전히 타입 공간의 기법이다.


[ITEM 22] 타입 좁히기

타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정으로 조건문을 통해서 타입을 좁혀나갈수 있다.

1. null 체크

2. 분기문에서 예외 던지기

3. instanceof 사용

4. Array.isArray같은 일부 내장 함수로도 타입을 좁힐 수 있음

타입스크립트는 일반적으로 조건문에서 타입을 좁히는데 매우 능숙하다.

profile
코드도 짜고, 근육도 짜고

0개의 댓글