본문으로 건너뛰기

01 타입스크립트 알아보기

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

  • 모든 자바스크립트는 타입스크립트지만, 모든 타입스크립트가 자바스크립트는 아니다.
    • TS가 타입을 명시하는 추가적인 문법을 가지기 때문

타입 체커

무엇이 문제인지 알겠나요?
let city = 'new york city';
console.log(city.toUppercase());

// VM359:2 Uncaught TypeError: city.toUppercase is not a function
타입스크립트의 타입 체커
let city = 'new york city';
console.log(city.toUppercase());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
  • 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것.
  • 타입스크립트가 '정적' 타입 시스템이라는 것은 바로 이런 특징 때문.
  • 그러나 타입 체커가 모든 오류를 찾아내지는 않는다.

의도와 다르게 동작하는 코드

무엇이 문제인지 알겠나요?
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에서 오타가 났음을 알 수 있다.
  • 즉, 오타가 났기 때문에 undefined가 났던 것
타입스크립트의 타입 체커
const states = [
{ name: 'Alabama', capitol: 'Montgomery' },
{ name: 'Alaska', capitol: 'Juneau' },
{ name: 'Arizona', capitol: 'Phoenix' },
// ...
];
for (const state of states) {
console.log(state.capital);
// ~~~~~~~ Property 'capital' does not exist on type
// '{ name: string; capitol: string; }'.
// Did you mean 'capitol'?
}
  • 하지만 어느 쪽이 오타인지 타입스크립트는 알지 못한다.
  • 이럴 때 명시적으로 states를 선언하여 의도를 분명히 한다.

의도를 분명히 한 코드

타입스크립트의 타입 체커
interface State {
name: string;
capital: string;
}

const states: State[] = [
{ name: 'Alabama', capitol: 'Montgomery' },
// ~~~~~~~~~~~~~~~~~~~~~
{ name: 'Alaska', capitol: 'Juneau' },
// ~~~~~~~~~~~~~~~~~
{ name: 'Arizona', capitol: 'Phoenix' },
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
// properties, but 'capitol' does not exist in type
// 'State'. Did you mean to write 'capital'?
// ...
];
for (const state of states) {
console.log(state.capital);
}
  • 어디서 오류가 발생했는지 알 수 있다.
  • 제시한 해결책이 올바르다.

요약

  1. TS는 JS의 상위집합이다.
  2. TS는 JS 런타임 동작을 모델링하는 타입 시스템을 갖고 있으므로, 런타임 오류를 발생시키는 코드를 찾아내려 한다. 그러나 타입 체커를 통과하면서도 런타임 오류를 발생시키는 코드는 존재할 수 있다. (ts를 맹신하지 말자)
  3. JS에서는 허용되나, TS에서 문제되는 경우가 있다. 이는 취향차이이다. (JS의 형 변환 특징에서 자유롭지 않음)

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

noImplicitAny

  • true일 경우, 명시적으로 any라고 선언하거나 더 분명한 타입을 사용한다.
  • TS는 타입 정보를 가질 때 가장 효과적이므로, 되도록 noImplicitAny를 사용하면 좋다.
// tsConfig: {"noImplicitAny":true}

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

strictNullChecks

  • null, undefined가 모든 타입에서 허용되는지 확인하는 설정
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}

const x: number = null;
// ~ null 형식은 number 형식에 할당할 수 없습니다.
명시적으로 의도를 드러내자
// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}

const x: number | null = null;
  • 코드 작성이 어렵다.
  • 이 설정을 쓰기로 결정하였다면, noImplicitAny 설정부터 한다.
  • 프로젝트가 커질수록 이 설정을 처음부터 하는 게 좋다.
노트

이 모든 설정을 하고 싶다면 strict 설정을 하라

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

타입스크립트 컴파일러의 역할

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

타입 오류가 있는 코드도 컴파일이 가능

타입스크립트의 오류는 warning이나 다름없다. 문제되는 부분을 알려주지만 빌드를 못하는 건 아니다.

오류가 있을 때 컴파일하지 않으려면 noEmitOnError 를 설정하자.

노트

코드에 오류가 있을 때 '컴파일에 문제가 있다'라고 하는 게 아니라, '타입 체크에 문제가 있다'라고 하는 게 맞다.

런타임에는 타입 체크가 불가능

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은 타입이므로 런타임 시점에 아무런 역할을 할 수 없음
  • 런타임에 타입 정보를 유지해야 함

속성 체크

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;
}

// 태그된 유니온(tagged union)
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;
}
}
  • kind 로 체크 중...

클래스

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

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
}
}
  • Rectangle를 클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다.
  • type Shape = Square | Rectangle;에서 Rectangle는 타입으로 참조되나, shape instanceof Rectangle는 값으로 참조된다.

타입 연산은 런타임에 영향을 주지 않음

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

타입 단언문으로 인해 타입체커를 통과하나 이는 잘못된 코드이다. string에 대한 타입 가드가 되어있지 않기 때문.

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

런타임 타입은 선언된 타입과 다를 수 있음

function setLightSwitch(value: boolean) {
switch (value) {
case true:
turnLightOn();
break;
case false:
turnLightOff();
break;
default:
console.log(`I'm afraid I can't do that.`);
}
}
  • console.log 가 실행될 여지가 있을까?
  • boolean은 런타임에 제거된다.
    • 매개변수에 boolean이 아닌 다른 값을 넣었다면...?

타입스크립트 타입으로는 함수를 오버로드 할 수 없음

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

  • 타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩은 불가능하다.
  • 기능은 지원하지만, 온전히 타입 수준에서만 동작한다.

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

  • JS 변환 시점에 제거되므로 런타임 성능에 영향이 없다.