Effective Typescript#1 타입스크립트 알아보기

Photo by Matt Duncan on Unsplash

Effective Typescript#1 타입스크립트 알아보기

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

자바스크립트는 타입스크립트의 서브셋이다.

자바스크립트 프로그램 이미 타입스크립트 프로그램이라고 볼 수 있습니다. 반대로 타입스크립트는 별도의 문법을 가지고 있기 때문에 자바스크립트 프로그램이 아닙니다. 심지어 app.js로 코딩되어 있는 코드를 app.ts로 전환변경하여 사용해도 무방합니다. 이는 타입스크립트로 마이그레이션 시 상당한 이점이 있습니다.

타입스크립트는 자바스크립트의 런타임 모델을 가지고 있다.

// 자바스크립트의 런타임 모델과 일치하는 부분
const x = 2 + '3'; // 정상
const y = '2' + 3; // 정상

// 타입스크립트만의 추가적인 동작
const a = null + 7; // TS 오류: 연산자 사용오류. JS는 7
const b = [] + 12; // TS 오류:연산자 사용오류. JS는 '12' (문자열임 🤮)
alert('Hello', 'Typescript'); // TS오류: 인자 개수 불일치. JS 정상

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

noImplicitAny 설정하기

암묵적인 any를 허용할 것인지에 대한 설정이며, 사용하지 않는 경우에는 타입스크립트의 타입 체커가 거의 무력해지게 됩니다. JS -> TS 마이그레이션을 하는 경우를 제외하고는 반드시 설정해야 할 값입니다.

noImplicitAny: false 인 경우에는 유효한 코드이지만, true인 경우에는 함수 매개변수의 타입이 암묵적으로 'any'라는 오류가 발생합니다.

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

strictNullChecks 설정하기

nullundefined가 모든 타입에서 허용되는지를 체크하는 설정입니다.

// `strictNullChecks: false` 인 경우 유효한 코드
// `strictNullChecks: true` 인 경우 `number` 타입에 `null` 을 할당할 수 없는 오류 발생
const x: number = null;

// `null` 또한 할당 가능한 타입으로 선언하여 할당 가능
const x: number | null = null;

JS에서 흔히 볼 수 있는 런타임 오류 Uncaught TypeError: Cannot read properties of undefined를 방지하기 위해서는 strictNullChecks설정이 권장됩니다.

strict 설정하기

타입스크립트에서 엄격한 타입체크를 하고 싶다면 모든 타입 관련된 체크를 포함하는 strict 옵션을 설정 해주는 것이 좋습니다. 8가지의 strict 관련 설정을 한번에 모두 설정하는 옵션입니다. 상세 참조

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

타입스크립트 컴파일러의 두 가지 역할. 이 두 가지는 완전히 독립적으로 동작합니다.

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

타입 오류가 있는 코드도 컴파일이 가능합니다.

$ cat test.ts
let x = 'hello';
x = 1234;
$ tsc test.ts
// type 오류 발생: '1234' 형식을 'string'에 할당할 수 없음

$ cat test.js
var x = 'hello';
x = 1234;

타입스크립트에서의 타입 오류는 다른 언어의 warning 수준으로 이해하면 됩니다. 타입 오류가 발생한다고 해서 빌드를 멈추지 않습니다. 오류가 있을 때 컴파일을 멈추고 싶다면 tsconfig.jsonnoEmitOnError을 설정하면 됩니다.

런타임에는 타입 체크가 불가능합니다.

타입스크립트에서의 타입들은 컴파일 과정에 모두 제거됩니다. instanceof는 런타임에 동작되는데 Rectangle은 단순 타입이기 때문에 체크할 수 없는 문제가 있습니다.(JS의 instanceof는 타입이 아닌 prototype의 형태를 체크하는 방식으로 동작하는 것 같은데, 세부 동작에 대한 이해가 필요합니다.)

interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

calcArea(shape: Shape) {
  if (shape instanceof Rectangle) { // 오류
    console.log('Rectangle');
  } else {
    console.log('Square');
  }
}

이를 해결하기 위한 여러가지 방법이 있습니다.

// 방법1. in 연산자 사용
if ('height' in shape) { 
  console.log('Rectangle');
} else {
  console.log('Square');
}

// 방법2. tagged union 사용
interface Square {
  kind: 'Squre';
  width: number;
}

interface Rectangle {
  kind: 'Rectangle';
  height: number;
}

type Shape = Square | Rectangle;

calcArea(shape: Shape) {
  if (shape.kind === 'Rectangle') {
    console.log('Rectangle');
  } else {
    console.log('Square');
  }
}

// 방법3. class 사용
class Square {
  width: number;
}

class Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

calcArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    console.log('Rectangle');
  } else {
    console.log('Square');
  }
}

class를 사용하는 방식일 때, type Shape = Square | Rectangle에서는 타입으로 참조되지만, shape instanceof Rectangle이 동작할 때는 값으로 참조됩니다. 어떻게 참조되는지 구분하는 것이 매우 중요합니다.(8장)

타입 연산은 런타임에 영향을 주지 않습니다.

아래 예시와 같이 타입 연산자 as는 컴파일과 런타임에 아무런 영향이 없습니다.

// TS
asNumber(val: number | string) {
  return val as number;
}

// JS
asNumber(val) {
  return val;
}

따라서 as사용 시 주의를 요하며 typeof연산자를 통하여 정확하게 타입 연산 및 타입 변경걍 수행해야 합니다.

function asNumber(val: number | string): number {
  return typeof(val) === 'string' ? Number(val) : val;
}

런타임 타입은 선언된 타입과 다를 수 있습니다.

런타임에는 모든 타입이 제거되기 때문에 setLightSwitch의 파라미터에 'On'이나 'Off'같은 값이 들어올 수도 있고 이에 따라서 의도하지 않게 동작할 수 있습니다.

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default: 
      console.log('실행 될지?')
  }

타입스크립트 타입은 런타임 성능에 영향을 주지 않습니다

타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에 런타임의 성능에는 아무런 영향이 들지 않습니다. '런타임' 오버헤드가 없는 대신, '빌드타임' 오버헤드가 있습니다. 빌드 오버헤드가 커지면 트랜스파일만(transplie only) 옵션을 설정하여 타입 체크를 건너뛸 수 있습니다.

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

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

calLength(v: Vector3D) {
  const a = v['x'];
  for (const axis of Object.keys(v)) {
    const coord = v[axis]; // 오류
    length += Math.abs(coord);
  }
  return length;
}
class C {
  foo: string;
  constructor(foo: string) {
    this.foo = foo;
  }
}

const c = new C('instance of C');
const d: C = { foo: 'instance of C' };

if (c === c) {
  console.log('c === c');
}

if (c === d) {
  console.log('c === d');
}
console.log(d instanceof C);
console.log(c);
console.log(d);

// 결과
c === c
false
C { foo: 'instance of C' }
{ foo: 'instance of C' }

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

let age: number;
age = '12'; // 오류
age = '12' as any; // 정상

타입 선언이 귀찮거나 노력을 쏟고 싶지 않아서 as any를 사용하고 싶은 경우가 많습니다. 그러나 any를 사용하면 타입스크립트의 수많은 장점을 누릴 수 없게 됩니다. 부득이 하게 사용하더라도 위험성을 알고 사용해야합니다.

any타입에는 타입 안정성이 없습니다

위 코드에서 아래의 코드를 추가할 경우 런타임에 오류가 발생하지 않고 정상 동작하게 됩니다. 심지어 아래 경우에 TS 타입 체커는 agenumber로 인식하여 굉장한 혼란을 가중시킵니다.

age += 1; // '121' 이지만 number 타입;

any는 함수 시그니처를 무시해 버립니다

any를 이용해서 함수에서 요구하는 시그니처의 타입을 무시할 수 있습니다.

function calc(birthdate: Date) : number { ... }
let birthdate: any = '1990-01-02';
calc(birthdate); // 정상

any 타입에는 언어 서비스가 적용되지 않습니다

언어/툴에서 제공하는 자동완성 기능으로 속성이 표시되지 않습니다. image.png

image.png

그리고 툴에서 제공하는 refactor 기능 중 하나인 Rename 기능 등을 지원 받을 수 없습니다. 이는 생산성에도 직결되는 문제입니다.

any는 타입 설계를 감춰버립니다

any 타입으로 선언 시에 타입의 설계가 불분명 해집니다. 예를 들어 어플리케이션의 상태를 나타내는 타입이 있는데 any 타입으로 선언되어 있다면 내부 속성이 어떻게 설계된 것인지 알기가 어렵습니다. 이는 특히 협업 시에 어려움이 발생합니다.

any는 타입시스템의 신뢰도를 떨어뜨립니다

타입 체커가 컴파일 타임에 사람의 실수를 잡아주고 코드의 신뢰도를 향상 시킵니다. 작성한 코드가 런타임에 오류를 발생시킨다면 더 이상 타입 체커를 신뢰할 수 없습니다. any타입을 쓰지 않으면 런타임에 발생할 오류를 미리 잡을 수 있습니다.

any 타입을 사용하면 타입체커와 타입스크립트 언어 서비스를 무력화시켜 버립니다. any 타입은 진짜 문제점을 감추며, 개발 경험을 나쁘게 하고, 타입 시스템의 신뢰도를 떨어뜨립니다. 최대한 사용을 지양합시다.