이펙티브 타입스크립트 - 2장(2)

연꽃·2022년 4월 25일
0

컴퓨터 서적

목록 보기
14/14
post-thumbnail

아이템 14 - 타입 연산과 제너릭 사용으로 반복 줄이기

  • DRY(don't repeqt yourserf)원칙을 타입에도 최대한 적용하자.
  • 타입들 간의 매핑을 위해 타입스크립트가 제공한 도구들(keyof,typeof,인덱싱, 매핑된 타입들)을 이용하자.
  • 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하자.
  • Pick, Partial, ReturnType 공부하자.

타입 중복은 코드 중복만큼 많은 문제를 발생시킨다. DRY원칙을 타입에 대해서도 적용해보자. 아래의 코드를 보자.

interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;
  lastName: string;
  birth: Date;
}

위 코드에 예를 들어 선택적 필드인 middleName을 Person에 추가한다면, Person과 PersonWithBirthDate는 다른 타입이 된다. 이에 대해서 아래의 코드처럼 extends를 사용하여 반복을 제거할 수 있다.

interface Person {
  firstName: string;
  lastName: string;
}
interface PersonWithBirthDate extends Person{
  birth: Date;
}

함수에 대해서도 중복을 줄여보자. 아래의 코드를 확인해 보자.

// HIDE
interface Options {}
// END
function get(url: string, opts: Options): Promise<Response> { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ }
function post(url: string, opts: Options): Promise<Response> { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ }

위 코드는 같은 시그니처를 공유하고 있다. 이 중복되는 시그니처를 명명된 타입으로 분리해 낼 수 있다.

// HIDE
interface Options {}
// END
type HTTPFunction = (url:string, options: Options) => Promise<Response>;
const get: HTTPFunction = (url, options) => {/* COMPRESS */ return Promise.resolve(new Response()); /* END */ };
const post: HTTPFunction = (url, options) => { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ };

태그된 유니온에서 제너릭 타입인 Pick을 활용하여 중복을 제거할 수 있다. 아래의 코드를 살펴보자.

interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';  // Repeated types!

위 코드에서 Pick을 활용해보자.

interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionRec = Pick<Action, 'type'>;

내용이 길고 어려운 것이 너무 많다. 다음에 더 공부하고 추가해보자.

아이템 15 - 동적 데이터에 인덱스 시그니처 사용하기

  • 런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 활용하자.

용어부터 짚고 넘아가자. 동적 데이터는 런타임 때까지 객체의 속성을 알 수 없는 데이터를 말한다. 인덱스 시그니처를... 도저히 이해하지 못하겠다.

아이템 16 - 타입 단언보다는 타입 선언을 활용하기

  • 타입 단언(as Type)보다 타입 선언(: Type)을 사용해야한다.
  • 타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에서는 타입 단언문과 null 아님 단언문을 사용한다.

타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 두 가지다.

interface Person { name: string };

const alice: Person = { name: 'Alice' };  // Type is Person
const bob = { name: 'Bob' } as Person;  // Type is Person

첫 번째는 변수에 타입 선언을 붙여서 그 값이 선언된 타입임을 명시한 것이고, 두 번째는 타입단언을 수행하여 타입스크립트가 추론한 타입이 있더라도 Person 타입으로 간주한다.

타입 단언보다 타입 선언을 사용하는 것이 좋은데 이유는 다음과 같다.

interface Person { name: string };
const alice: Person = {};
   // ~~~~~ Property 'name' is missing in type '{}'
   //       but required in type 'Person'
const bob = {} as Person;  // No error

타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사하여 오류가 있는 경우 오류를 표시하지만, 타입 단언은 강제로 타입을 저정하여 타입 체커에게 오류를 무시하라고 한다.

속성을 추가하는 경우에도 타입 선언과 타입 단언은 차이가 있다.

interface Person { name: string };
const alice: Person = {
  name: 'Alice',
  occupation: 'TypeScript developer'
// ~~~~~~~~~ Object literal may only specify known properties
//           and 'occupation' does not exist in type 'Person'
};
const bob = {
  name: 'Bob',
  occupation: 'JavaScript developer'
} as Person;  // No error

타입 선언문에서는 잉여 속성 체크가 동작했지만, 단언문에서는 그렇지 않았다. 이러한 이유로 꼭 필요한 경우가 아니라면, 타입 선언을 사용하자.

화살표 함수의 타입 선언은 추론된 타입이 모호할 때가 있다. 아래의 코드를 살펴보자.

interface Person { name: string };
const people = ['alice', 'bob', 'jan'].map(name => ({name}));
// { name: string; }[]... but we want Person[]

people의 결과는 Person[]이 아닌 {name:string;}이다. 이를 해결하기 위해 다음과 같은 코드를 살펴보자.

interface Person { name: string };
const people = ['alice', 'bob', 'jan'].map(
  (name): Person => ({name})
); // Type is Person[]

위 코드에서 people의 타입은 Person[]이 된다. 그런데 여기서 소괄호에 따라서 다른 의미를 갖는다. 위 코드처럼 (name): Person의 경우에는 name의 타입은 없고, 반환되는 타입이 Person이라는 의미이다. 반면, (name:Person)의 경우에는 name이 Person타입이고 반환 타입이 없다는 의미이기 때문에 오류를 발생시킨다.

아이템 10 - 객체 래퍼 타입 피하기

  • 타입스크립트는 객체 래퍼 타입은 지양하고, 대신 기본형 타입을 사용해야 한다. 예를 들어, String 대신 string, Number 대신 number, Boolean 대신 boolean을 사용해야 한다.

먼저 래퍼 객체란 이름처럼 원시 타입의 값을 감싸는 형태의 객체이다. number, string, boolean, symbol 데이터 타입에 각각 대응하는 Number, String, Boolean, Symbol이 제공된다.

자바스크립트의 문자열은 원시 타입으로 존재한다. 우리가 문자열의 프로퍼티에 접근하려고 할 때, 자바스크립트는 new String을 호출한 것처럼 문자열 값을 객체로 변환고 이 객체를 래퍼 객체라고 한다. 래퍼 객체는 프로퍼티를 참조할 때 생성되며 프로퍼티 참조가 끝나면 사라진다. 아래의 예시를 살펴보자.

var str = 'sunday';
str.length = 6; // new String(str).length = 6

console.log(str.length); // undefined

변수의 프로퍼티에 접근할 때 래퍼 객체가 임시로 생성된다. 프로퍼티의 값을 할당하는 것은 임시로 생성된 래퍼 객체에서 수행되며 지속되지 않는다. 이 때문에 원시 타입의 프로퍼티(실제로는 래퍼 객체의 프로퍼티)가 마치 읽기 전용 값처럼 존재하는 것이다.

특히, string을 사용할 때 유의해야 한다. 아래의 예시를 살펴보자.

function getStringLen(foo: String) {
  return foo.length;
}

getStringLen("hello");  // OK
getStringLen(new String("hello"));  // OK

위 코드는 잘 동작하는 것처럼 보이지만, string을 매개변수로 받는 메서드에서 String 객체를 전달할 때 문제가 발생한다.

function isGreeting(phrase: String) {
  return [
    'hello',
    'good day'
  ].includes(phrase);
          // ~~~~~~
          // Argument of type 'String' is not assignable to parameter
          // of type 'string'.
          // 'string' is a primitive, but 'String' is a wrapper object;
          // prefer using 'string' when possible
}

이는 string은 String에 할당할 수 있지만, String은 string에 할당할 수 없기 때문에 나타나는 오류이다. 따라서 string 타입을 사용해야 한다.

아이템 11 - 잉여 속성 체크의 한계 인지하기

  • 객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행된다.

잉여 속성 체크는 타입이 명시된 변수에 객체 리터럴을 할당할 때, 타입스크립트가 해당 타입의 속성이 있는지 그리고 그 외의 속성은 없는지 확인하는 것이다. 아래의 예시를 살펴보자.

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}
const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
//                    and 'elephant' does not exist in type 'Room'
};

Room 타입에 elephant라는 속성이 추가되어 오류가 발생한 상황이다. <<구조적 타이핑 관점에서는 오류가 발생하지 않아야 한다>> 임시 변수를 도입해, 잉여 속성 체크를 생략하는 방법이 있다.

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}
const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
};
const r: Room = obj;  // OK

obj라는 임시 변수를 도입하여 오류를 제거한 상황이다. obj 타입은 Room 타입의 부분 집합이므로 Room에 할당 가능하며 타입 체커도 통과한다.
<<잉여 속성 체크와 할당 가능성 검사와는 별도의 과정이고, 구조적 타입 시스템과 연관이 있는 것 같은데 이 부분은 아직 잘 모르겠다...>>

아이템 12 - 함수 표현식에 타입 적용하기

  • 매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋다.

  • 만약 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해 내거나 이미 존재하는 타입을 찾아보자.

  • 다른 함수의 시그니처를 참조하려면 typeof fn을 사용한다.

    자바스크립트에서 함수 선언식과 표현식은 조금 다르다. 그리고 함수 표현식을 사용하는 것이 좋은데 이는 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점 때문이다.

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { /* COMPRESS */ return 0; /* END */ };

위의 코드는 첫번째 줄에서 함수의 타입을 선언한 것이고, 두번째 줄에서 함수 표현식을 통해 함수를 작성한 것이다.

아이템 13 - 타입과 인터페이스의 차이점 알기

  • 타입과 인터페이스의 차이점과 비슷한 점을 이해해야 한다.

타입스크립트에서 명명된 타입을 정의하는 방법은 '타입'을 사용하는 방법과 '인터페이스'를 사용하는 방법 두 가지이다. 그리고 대부분의 경우 타입을 사용해도 되고 인터페이스를 사용해도 된다.

type TState = {
  name: string;
  capital: string;
}
interface IState {
  name: string;
  capital: string;
}

구체적으로 인터페이스와 타입의 차이점에 대해 알아보자. 먼저 유니온 타입은 있지만, 유니온 인터페이스는 없다. 인터페이스는 타입을 확장할 수 있지만, 유니온은 할 수 없다.

또한, 인터페이스는 타입이 할 수 없는 '보강'을 할 수 있다. 다음 코드를 살펴보자.

interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
};  // OK

위 코드는 기존의 IState에 population이라는 속성을 보강한 것이다.

그렇다면 타입과 인터페이스 중 어느 것을 사용해야 할까? 복잡한 타입의 경우는 타입 별칭을 사용한다. 전체 코드의 일관성도 고려해야 한다. 또한 스타일이 확립되지 않은 프로젝트에서는 향후 보강의 가능성에 대해 생각해봐야 한다. 어떤 API에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는 것이 좋다. 왜냐하면 API가 변경될 때, 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있기 때문이다. 하지만 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계이므로 타입을 사용해야 한다.(정확하게 무슨 말인지 잘 모르겠음)

참고 : 이펙티브 타입스크립트

profile
우물에서 자라나는 중

0개의 댓글