이펙티브 타입스크립트 - 1장

연꽃·2022년 4월 21일
0

컴퓨터 서적

목록 보기
12/14
post-thumbnail

아이템 1 - 타입스크립트와 자바스크립티의 관계 이해하기

  • 타입스크립트는 자바스크립트의 상위집합이다. 즉, 모든 자바스크립트 프로그램은 타입스크립트 프로그램에 포함된다.
  • 타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입시스템을 가지고 있기 때문에 런타임 오류를 발생시키는 코드를 찾아내려고 한다. 하지만 모든 오류를 찾는 것은 아니다.
  • 타입스크립트 타입 시스템은 전반적으로 자바스크립트 동작을 '모델링'한다. 하지만 자바스크립트에서는 허용되지만 타입스크립트에서는 문제가 되는 경우도 있다.

이 코드를 실행하면

const states = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capital: 'Juneau'},
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];
for (const state of states) {
  console.log(state.capitol);
}

다음과 같은 결과가 나타난다.

undefined
undefined
undefined

이 코드는 자바스크립트에서는 오류 없이 실행이 된다. 하지만 반복문 내의 state.capitol는 일반적으로 state.capital일 것이다. 타입스크립트의 경우,
다음과 같이 오류를 잡아낸다.

const states = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capital: 'Juneau'},
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];
// END

for (const state of states) {
  console.log(state.capitol);
                 // ~~~~~~~ Property 'capitol' does not exist on type
                 //         '{ name: string; capital: string; }'.
                 //         Did you mean 'capital'?
}

다음 예시를 살펴보자.

const x = 2 + '3';  // OK, type is string
const y = '2' + 3;  // OK, type is string

위 코드는 다른 언어였다면 오류가 될만한 코드이다. 하지만 결과는 모두 문자열 '23'이 되는 자바스크립트 런티임 동작으로 모델링이 된다.
반대로 정상 동작하는 코드에 오류가 생기는 경우도 있다!

const a = null + 7;  // Evaluates to 7 in JS
      // ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12;  // Evaluates to '12' in JS
      // ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript');  // alerts "Hello"
           // ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2

위 코드는 런타임 오류가 발생하지 않지만, 타입 체커는 문제점을 표시한다.

아이템 2 - 타입스크립트 설정 이해하기

  • 타입스크립트는 타입 정보를 가질 때 가장 효과적이기 때문에, 되도록이면 noImplicitAny를 설정해야 한다.
  • strictNullChecks는 null과 undefined 관련 오류를 잡아내는데 많은 도움이 된다.

noImplicitAny 설정에 따라 오류가 나고 나지 않을 수 있다. 이 설정은 타입스크립트가 문제를 발견하기 수월해지고, 가독성이 좋아지며, 개발자의 생산성이 향상된다.

// tsConfig: {"noImplicitAny":false}

function add(a, b) {
  return a + b;
}
// tsConfig: {"noImplicitAny":true}

function add(a, b) {
          // ~    Parameter 'a' implicitly has an 'any' type
          //    ~ Parameter 'b' implicitly has an 'any' type
  return a + b;
}

위 두 코드는 같은 코드이지만 설정에 따라서 오류의 여부가 달라진 다는 것을 알수 있다. 그래서 밑의 코드가 가장 명확하고 좋다.

// tsConfig: {"noImplicitAny":true}

function add(a: number, b: number) {
  return a + b;
}

strictNullChecks 설정은 null과 undefined가 모든 타입에서 허용되는지 확인한다. 아래의 코드를 살펴보자. 이는 strictNullChecks을 설정하지 않을 경우, 유효한 코드임을 확인할 수 있다.

// tsConfig: {"noImplicitAny":true,"strictNullChecks":false}

const x: number = null;  // OK, null is a valid number

하지만 반대로, 아래의 코드는 strictNullChecks 설정을 하여 null을 허용하지 않아 오류가 나는 코드이다. undefined의 경우에도 null과 같은 오류가 발생한다.

// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}

const x: number = null;
//    ~ Type 'null' is not assignable to type 'number'

이는 다음과 같이, null을 허용한다는 명시적인 코드를 통해서 오류를 해결할 수 있다.

// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}

const x: number | null = null;

아이템 3 - 코드 생성과 타입이 관계없음을 이해하기

  • 타입스크립트 컴파일러는 1. 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일(translate+compile)하고 2. 코드의 타입 오류를 체크한다. 그런데 이 두 가지는 독립적으로 수행된다.
  • 따라서 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않아, 타입 오류가 존재하여도 코드 생성(컴파일)은 가능하다.
  • 결국 타입스크립트 타입은 런타임에 사용되지 않아, 런타임 타입과 선언된 타입이 다를 수 있다. 이러한 혼란스러운 상황을 피하기 위해서는 일반적으로 태그된 유니온과 속성 체크 방법을 사용하거나, 클래스를 활용하여 타입스크립트 타입과 런타임 값 둘다 제공하는 방법이 있다.
interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
                    // ~~~~~~~~~ 'Rectangle' only refers to a type,
                    //           but is being used as a value here
    return shape.width * shape.height;
                    //         ~~~~~~ Property 'height' does not exist
                    //                on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}

위 예시에서 instanceof에 대한 체크는 런타임에 일어나지만, Rectangle은 값이 아니라 타입이기 때문에 런티임 시점에 아무런 역할을 할 수가 없다. 자바스크립트가 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 모두 제거되어 버리는 일이 발생한다.

위 코드에 대해 shape 타입을 명확하게 하기 위해서는 런타임에 타입 정보를 유지해야한다. 먼저 아래와 같이 shape 내에 height 속성의 존재 유무를 체크하는 것으로 가능하다.

function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }

다음으로 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 '태그'기법이 있다. 이 방법은 런타임에 타입 정보를 손쉽게 유지할 수 있기 때문에, 타입스크립트에서 흔히 볼 수 있다!!!

interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}

마지막으로 타입으로만 사용 가능한 interface 대신에 타입과 값 모두로 사용할 수 있는 class를 이용하는 방법이다.

class Square {
  constructor(public width: number) {}
}
class Rectangle extends Square {
  constructor(public width: number, public height: number) {
    super(width);
  }
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;  // OK
  }
}

아이템 4 - 구조적 타이핑에 익숙해지기

이 파트 잘 이해 되지 않아서 일단 넘어가기

아이템 5 - any 타입 지양하기

  • any 타입은 타입 체커와 타입스크립트 언어 서비스를 무력화 시키기 때문에 지양해야 한다.

any 타입에는 타입 안정성이 없다. 다음과 같은 예시를 살펴보자.

   let age: number;
   age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
   age = '12' as any;  // OK
age += 1;  // OK; at runtime, age is now "121"

원래 number타입을 any타입으로 바꾸었기 때문에 age는 '12'를 할당받아, string타입으로 바뀌어 age에 1을 더할 때 '121'이 된다!

또한 any 타입은 함수 시그니처를 무시한다. 함수를 호출하는 쪽은 약속된 타입의 입력을 제공하고, 함수는 약속된 타입의 출력을 반환한다. 그러나 any 타입은 이 약속을 어겨 함수의 시그니처를 무시할 수 있다.

function calculateAge(birthDate: Date): number {
  // COMPRESS
  return 0;
  // END
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // OK

이 함수의 시그니처는 매개변수가 Date 타입이어야 한다는 것이다. 하지만 위 코드에서는 any 타입을 사용하여 이 시그니처를 무시한 것이다.

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

profile
우물에서 자라나는 중

0개의 댓글