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

연꽃·2022년 4월 24일
0

컴퓨터 서적

목록 보기
13/14
post-thumbnail

아이템 7 - 타입이 값들의 집합이라고 생각하기

  • 타입을 값의 집합으로 생각하자. 이 집합은 유한(boolean 또는 리터럴 타입)하거나 무한(number 도는 string)하다.
  • 타입스크립트 타입은 엄격한 상속관계가 아니라 겹쳐지는 집합으로 표현된다.

타입스크립트에서 가장 작은 집합의 타입은 never타입이다. 다음으로 한 가지 값만 포함하는 unit타입, 두 개 혹은 세 개로 묶은 union타입이 있다.

//never
const x: never = 12;
   // ~ Type '12' is not assignable to type 'never'
//unit
type A = 'A';
type B = 'B';
type Twelve = 12;
//union
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;

위 코드들의 타입은 범위가 한정되어 있지만, 대부분의 타입은 범위가 무한대이다.

타입은 값의 집합이다. 다음과 같은 코드르 살펴보자.

interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan;

&연산자는 교집합을 계산하므로 PersonSpan 타입은 공집합인 것처럼 보인다! 하지만 타입 연산자는 인터페이스 속성이 아닌, 값의 집합(타입의 범위)에 적용된다. 그리고 추가적인 속성을 가지는 값도 여전히 그 타입에 속한다. 그래서 Person과 Lifespan을 둘 다 가지는 값은 인터섹션 타입에 속하게 된다.

아이템 8 - 타입 공간과 값 공간의 심벌 구분하기

  • 타입스크립트 코드를 읽을 때 타입인지 값인지 구분해야 한다.
  • 모든 값은 타입을 가지지만, 타입은 값을 가지지 않는다. type과 interface 같은 키워드는 타입 공간에만 존재한다.

타입스크립트의 심벌은 타입 공간이나 값 공간 중의 한 곳에 존재한다. 심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있다.

interface Cylinder {
  radius: number;
  height: number;
}

const Cylinder = (radius: number, height: number) => ({radius, height});

위 코드에서 interface Cylinder는 타입으로 쓰이고, const Cylinder는 값으로 쓰였다. 이름만 같을 뿐이지 서로 관련이 없으며 타입으로 혹은 값으로 쓰일 수 있다. => 그러면 이렇게 심벌을 만드는 것은 잘못된 것 아닌가? 왜 이런 예시가 나오는지 잘 모르겠네...

typeof는 타입에서 쓰일 때와 값에서 쓰일 때, 다른 기능을 하는 연산자들 중 하나이다.

type T1 = typeof p;  // Type is Person
type T2 = typeof email;
    // Type is (p: Person, subject: string, body: string) => Response

const v1 = typeof p;  // Value is "object"
const v2 = typeof email;  // Value is "function"

타입의 관점에서, typeof는 값을 읽어서 타입스크립트 타입을 반환한다. 반면 값의 관점에서 typeof는 자바스크립트 런타임의 typeof 연산자가 된다.

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

  • 타입 단언(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개의 댓글