새소식

Typescript

[이펙티브 타입스크립트] 1. 타입스크립트 알아보기

  • -

 

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

 

▪️  타입스크립트는 문법적으로 자바스크립트의 상위집합이다.

 

-> 자바스크립트 프로그램에 문법 오류가 없다면 유효한 타입스크립트 프로그램이다.

-> 자바스크립트 프로그램에 어떤 이슈가 존재한다면 문법 오류가 아니더라도 타입체커에게 지적당할 가능성이 높다.

-> 그러나 문법의 유효성과 동작 이슈는 독립적인 문제이다.

 

 

▪️ 타입시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다.

 

-> 그러나 타입체커가 모든 오류를 찾아내지는 않는다.

-> 타입체커를 통과하지만 런타임 오류를 발생시키는 코드는 존재한다.

=> 모든 자바스크립트는 타입스크립트지만, 일부 자바스크립트만이 타입체크를 통과한다.

 

 

▪️ 타입스크립트 타입시스템은 자바스크립트의 런타임 동작을 모델링한다.

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

위 예제는 다른언어 였다면 런타임 오류가 될 만한 코드이나, 모두 문자열 "23" 이 되는 자바스크립트 런타임 동작으로 모델링된다.

 

 

 

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

 

▪️ noImplicitAny 설정은 변수들이 미리 정의된 타입을 가져야하는지 여부를 제어한다.

 

-> any를 코드에 넣지 않았지만, any 타입으로 간주되는 것을 '암시적 any' 라고 부른다.

-> 타입스크립트는 타입 정보를 가질 때 가장 효과적이기 때문에 되도록이면 noImplicitAny 설정을 해주는 것이 좋다

 

 

▪️ strictNullChecks는 null과 undefined가 모든 타입에서 허용되는지 확인하는 설정이다.

strickNullChecks : true
const x: number = null;  //null은 할당할 수 없음
const x: number | null = null;  //가능

->strictNullChecks를 설정하려면 noImplicitAny를 먼저 설정해야한다.

-> "undefined는 객체가 아닙니다" 와 같은 런타임 오류를 방지하기 위해 strickNullChecks를 설정하는 것이 좋다.

 

 

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

 

큰 그림에서 보면 타입스크립트 컴파일러는 두 가지 역할을 수행한다.

  • 최신 타입스크립트/ 자바스크립트를 브라우저에서 동작 할 수 있도록 구 버전의 자바스크립트로 트랜스파일한다.
  • 코드의 타입오류를 체크한다.

 

✔️ 타입 오류가 있는 코드도 컴파일이 가능하다.

-> 타입스크립트의 오류는 C나 자바와 같은 언어들의 경고와 비슷하다. 문제가 될만한 부분을 알려주지만, 그렇다고 빌드를 멈추지 않는다.

     (만약 오류가 있을 때 컴파일 하지 않으려면 tsconfig에 noEmitOnError 설정)

 

 

✔️런타임에는 타입체크가 불가능하다. 

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
                    // ~~~~~~~~~ 'Rectangle' 은(는) 형식만 참조하지만,
                    //           여기서는 값으로 사용되고 있습니다.
    return shape.width * shape.height;
                    //         ~~~~~~ 'Shape' 형식에 'height' 속성이 없습니다.
                    //          
  } else {
    return shape.width * shape.width;
  }
}

instanceof 체크는 런타임에 발생한다.

Rectangle은 타입이기 때문에 런타임 시점에서 아무런 역할을 할 수 없다. 

타입스크립트의 타입은 '제거 가능(erasable)'하다.

실제로 자바스크립트로 컴파일 되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거되어버린다.

 

위의 shape 타입을 명확하게 하려면, 런타임에 타입정보를 유지한은 방법이 필요하다.

그 방법은 다음과 같다.

 

첫번째, height 속성이 존재하는지 체크해본다.

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape){
	if('height' in shape) {
    	shape;   // 타입이 Rectangle
        return shape.width * shape.height;
    } else {
      shape;  //타입이 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;  // 타입이 Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // 타입이 Square
    return shape.width * shape.width;
  }
}

여기서 type Shape은 '태그된 유니온(tagged union)'의 한 예이다.

이는 런타임에 타입정보를 손쉽게 유지할 수 있기 때문에 타입스크립트에서 흔하게 볼 수 있다.

 

 

세번째, 타입을 클래스로 만든다.

이는 타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법이다.

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; // 타입이 Rectangle
        return shape.width * shape.height;
    } else {
        shape; // 타입이 Square
        return shape.width * shape.width; //정상
    }
}

Rectangle을 클래스로 선언하면 타입과 값으로 모두 사용할 수 있다.

type Shape = Square | Rectangle 부분에서 Rectangle은 타입으로 참조되지만,

shpae instanceof Rectangel 부분에서는 값으로 참조된다.

 

 

✔️ 타입 연산은 런타임에 영향을 주지 않는다.

 

✔️ 런타임 타입은 선언된 타입과 다를 수 있다. 

 

✔️ 타입스크립트 타입으로는 함수를 오버로드 할 수 없다.

-> 함수 오버로딩 : 동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용하는 것.

-> 타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩은 불가능 하다.

-> 타입스크립트에서 함수 오버로딩은 온전히 타입 수준에서만 동작한다.

-> 여러개의 선언문을 작성할 수는 있지만 구현체는 오직 하나 뿐이다.

 

✔️ 타입스크립트의 타입은 런타임 성능에 영향을 주지 않는다.

-> 타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에, 런타임의 성능에 아무런 영향을 주지 않는다.

 

 

 

 

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

 

덕타이핑(duck typing) : 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우, 객체를 해당 타입에 속하는 것으로 간주하는 방식이다.

 

자바스크립트는 본질적으로 덕 타이핑(duck typing) 기반이다.

만약 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어졌는지 신경쓰지 않고 사용한다.

 

interface Vector2D {
  x: number;
  y: number;
}

// 벡터의 길이를 구하는 함수 (2D)
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

// 3D 벡터의 길이를 1로 만드는 정규화 함수
function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

// 함수 normalize는 1보다 조금 더 긴 길이를 가진 결과를 출력
normalize({x: 3, y: 4, z: 5})
// {x: 0.6, y: 0.8, z: 1}

 위 코드에서 타입스크립트는 오류를 잡지 못했다.

calculateLength는 2D 벡터를 기반으로 연산하는데, 버그로 인해 normalize가 3D 백터로 연산되었다.

z가 정규화에서 무시된 것이다.

그런데도 타입 체커가 이 문제를 잡아내지 못했다.

 

이유는 Vector3D와 호환되는 {x, y, z} 객체로 calculateLength를 호출하면, 구조적 타이핑 관점에서 x와 y가 있어서 Vetor2D와 호환된다. 따라서 오류가 발생하지 않았고, 타입 체커가 문제로 인식하지 않았다. 

 

구조적 타이핑은 클래서와 관련된 할당문에서도 당황스러운 결과를 보여준다.

class C {
    foo: string;
    constructor(foo: string) {
        this.foo = foo;
    }
}

const c = new C("instanceof C");
const d: C = {foo: "object literal"};    //정상

d가 C타입에 할당 되는 이유는?

d는 string 타입의 foo 속성을 가진다. 또한 매개변수로 호출이 되는 생성자를 가진다.

그래서 구조적으로는 필요한 속성과 생성자가 존재하기 때문에 문제가 없다. 

 

만약 C의 생성자에 단순 할당이 아닌 연산 로직이 존재한다면, d의 경우는 생성자를 실행하지 않음으로 문제가 발생하게 된다.

이러한 부분이 C타입의 매개변수를 선언하여 C 또는 서브클래스임을 보장하는 C++이나 자바 같은 언어와 매우 다른 특징이다.

 

 

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

 

any를 사용하면 타입스크립트의 수많은 장점을 누릴 수 없게 된다.

만약 부득이하게 any를 사용하더라도 그 위험성을 알고 있어야한다.

 

▪️ any 타입에는 타입 안정성이 없다. 

 

▪️ any는 함수 시그니처를 무시한다. 

   let age: number;
   age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
   age = '12' as any;  // OK
function calculateAge(birthDate: Date): number {
  // COMPRESS
  return 0;
  // END
}

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

위 코드에서는 birthData 매개변수는 string이 아닌 Data 타입이어야한다. any타입을 사용하게 되면 calculateAge의 시그니처를 무시하게 된다. 

 

▪️ any 타입에는 언어 서비스가 적용되지 않는다.

▪️ any 타입은 코드 리팩토링 때 버그를 감춘다.

▪️ any 는 타입 설계를 감춰버린다.

▪️ any는 타입시스템의 신뢰도를 떨어뜨린다.

-> any 타입을 쓰지 않으면 런타임에 발견될 오류를 미리 잡을 수 있고 신뢰도를 높일 수 있다.

 

 

 

Ref

<이펙티브 타입스크립트> 댄 밴더캄 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.