이펙티브 타입스크립트 - 01

1장. 타입스크립트 알아보기

1장에서는 타입스크립트란 무엇이고, 타입스크립트를 어떻게 사용해야 하는지, 자바스크립트와는 어떤 관계인지, 타입스크립트의 타입들은 null이 가능한지, any타입에서는 어떨지, 덕 타이핑이 가능한지 등을 알아보자.

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

  • 타입스크립트는 자바스크립트의 상위 집합(superset)이다. 자바스크립트는 타입스크립트의 부분집합(subset)이다.

  • 타입스크립트는 타입이 정의된 자바스크립트의 상위 집합이다.

자바스크립트 프로그램에 문법 오류가 없다면, 유효한 타입스크립트 프로그램이라고 할 수 있다.
그렇기 때문에 .js 파일을 .ts 로 변경해도 달라지는 것은 없다. 이러한 특성은 자바스크립트에서 타입스크립트로 마이그레이션 하는데 엄청난 이점이 된다.

모든 자바스크립트 프로그램은 타입스크립트이다 → 참
모든 타입스크립트 프로그램은 자바스크립트이다 → 거짓

image

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

타입스크립트는 타입 구문없이도 오류를 잡을 수 있지만 타입구문을 추가해 코드의 의도가 무엇인지 타입스크립트에게 알려주면 훨씬 더 많은 오류를 찾아낼 수 있다.

// bad
const states = [
  { name: 'Alabama', capitol: 'Montgomery' },
  { name: 'Alaska', capitol: 'Juneau' },
  { name: 'Arizona', capitol: 'Phoenix' },
  //...
]
 
for (const state of states) {
  console.log(state.capital)
  // Error: 'capital'속성이 {name: string, capitol: string;}형식에 없습니다. 'capitol을 사용하겠습니까?
 
  //...
}
 
// good
interface State {
  name: string
  capital: string
}
const states: State[] = [
  { name: 'Alabama', capitol: 'Montgomery' }, // Error: State타입에 'capitol'이 없다. 'capital'을 쓰려고 했습니까?
  { name: 'Alaska', capitol: 'Juneau' }, // Error: State타입에 'capitol'이 없다. 'capital'을 쓰려고 했습니까?
  { name: 'Arizona', capitol: 'Phoenix' }, // Error: State타입에 'capitol'이 없다. 'capital'을 쓰려고 했습니까?
  //...
]
 
for (const state of states) {
  console.log(state.capital)
}

처음에는 한곳에서는 capitol, 다른 한곳에서는 capital로 다르게 타이핑 했지만, 오류의 원인은 추측할 수 있겠지만 어느쪽이 오타인지 판별하지 못한다.

따라서 명시적으로 State타입을 선언하면 오류가 어디에서 발생했는지 찾을 수 있고, 제대로 된 해결책을 알려주는 것을 확인할 수 있다.

📕 요약

  • 타입스크립트는 자바스크립트의 상위 집합니다.
  • 타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입 시스템을 가지고 있기 떄문에 런타임 오류를 발생시키는 코드를 찾아내려고 한다.

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

타입스크립트 컴파일러는 매우 많은 설정을 가지고있는데 이번에는 noImplicitAnystrictNullChecks에 대해서 알아보자.

나머지 설정에 대해서는 따로 알아보기로 하자!!

1. noImplicitAny

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

  • noImplicitAnyfalse일 때, add함수에 마우스를 올려서 확인해보면, 타입스크립트가 a, b 파라미터를 any타입으로 추론하고 있는것을 확인할 수 있다.

  • noImplicitAnytrue일 때, 같은 코드임에도 불구하고 오류로 인식한다. 이 오류들은 명시적으로 : any라고 선언해 주거나 : number와 같은 더 분명한 타입을 사용하면 해결할 수 있다.

// noImplicitAny: false
function add(a, b) {
  // OK
  return a + b
}
 
// noImplicitAny: true
// bad
function add(a, b) {
  // Error: Parameter 'a' implicitly has an 'any' type
  return a + b
}
 
// noImplicitAny: true
// good
function add(a: number, b: number) {
  return a + b
}

타입스크립트는 타입정보를 가질 때 효과적이기 때문에, 되도록이면 noImplicitAny를 설정하고, 타입을 명시해주도록 하자!!

2. strictNullChecks

strictNullChecksnullundefined가 모든 타입에서 허용되는지 확인하는 설정이다.

  • strictNullChecksfalse일 때, null은 유효한 값이고 정상으로 표시된다.

  • strictNullCheckstrue일 때, null타입은 number타입에 할당할 수 없다는 오류가 나타나는 것을 확인할 수 있다. null대신 undefined를 사용해도 같은 오류가 나타날 것이다. 만약에 null이나 undefined를 허용하고 싶다면, 의도를 명시적으로 작성해 오류를 고칠 수 있다.

// strictNullChecks: false
const x: number = null
 
// strictNullChecks: true
// bad
const x: number = null // Error: Type 'null' is not assignable to type 'number'
 
// strictNullChecks: true
// good
const x: number | null = null

3. strict

strict설정은 타입스크립트에서 엄격한 체크를 할 수 있게 도와준다.

/* Strict Type-Checking Options */
"strict": true,
// "noImplicitAny": true,
// "strictNullChecks": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "strictPropertyInitialization": true,
// "noImplicitThis": true,
// "alwaysStrict": true,

"strict": true 설정은, 위의 주석 처리된 옵션들을 모두 활성화한 것과 같은 설정이다.

이 설정은 타입스크립트가 엄격하게 타입을 체크할 수 있도록 도와주기 때문에 "strict": true로 설정하고 개발을 하는 것이 좋다.

📕 요약

  • 타입스크립트 설정은 커맨드 라인을 이용하기 보다 tsconfig.json을 사용하는 것이 좋다.
  • 타입스크립트에서 엄격한 체크를 하고 싶다면 strict설정을 생각하자.

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

타입스크립트 컴파일러는 크게 아래 2가지 역할을 수행하고, 이 두가지는 서로 완벽히 독립적으로 동작한다.

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

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

컴파일은 타입체크와 독립적으로 동작하기 때문에, 타입오류가 있는 코드도 컴파일이 가능하다.

// index.ts
let x = 'hello'
x = 1234 // Error: number타입은 string타입에 할당할 수 없다.
 
// index.js
let x = 'hello'
x = 1234

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

만약, 오류가 있을 때 컴파일 하지 않으려면 tsconfig.jsonnoEmitOnError를 설정해주면 오류가 있을때는 컴파일을 하지 않도록 설정할 수 있다.

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

// index.ts
interface Square {
  width: number
}
 
interface Rectangle extends Square {
  height: number
}
 
type Shape = Square | Rectangle
 
function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    // Error: 'Rectangle는 형식만 참조하지만, 여기서는 값으로 사용되고 있다.
    return shape.width * shape.height // Error: 'Shape'타입에 'height'속성이 없다.
  } else {
    return shape.width * shape.width
  }
}
 
// index.js
function calculateArea(shape) {
  if (shape instanceof Rectangle) {
    return shape.width * shape.height
  } else {
    return shape.width * shape.width
  }
}

instanceof체크는 런타임에서 일어나지만, Rectangle은 타입이기 때문에 런타임에서 아무런 역할을 할 수 없다.

실제로 자바스크립트로 컴파일 되는과정에서 모든 인터페이스, 타입, 타입구문은 그냥 제거되기 때문에, 런타임에서는 Rectangle이 어떤값인지 모르기때문에 에러를 나타내는 것을 확인할 수 있다.

여기서 shape타입을 명확하게 하려면 런타임에 타입정보를 유지하는 방법이 필요한데, 3가지 방법을 알아보자.

2-1. 속성이 존재하는지 체크

첫번째 방법은, Shape타입에 height속성이 존재하는지 체크하는 방법이다.

// index.ts
// ...
function calculateArea(shape: Shape) {
  if ('height' in shape) {
    // shape 타입이 Rectangle
    return shape.width * shape.height
  } else {
    // shape 타입이 Square
    return shape.width * shape.width
  }
}
 
// index.js
function calculateArea(shape) {
  if ('height' in shape) {
    return shape.width * shape.height
  } else {
    return shape.width * shape.width
  }
}

2-2. 타입정보를 명시적으로 저장

두번째 방법은, 런타임에 접근가능한 타입정보를 명시적으로 저장하는 태그 기법이다.

// index.ts
interface Square {
  kind: 'square'
  width: number
}
 
interface Rectangle {
  kind: 'rectangle'
  width: number
  height: 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
  }
}
 
// index.js
function calculateArea(shape) {
  if (shape.kind === 'rectangle') {
    return shape.width * shape.height
  } else {
    return shape.width * shape.width
  }
}

2-3. 타입을 클래스로 작성

타입을 클래스로 만들면 타입(런타임 접근불가)와 값(런타임 접근가능)을 둘 다 사용할 수 있게 된다.

// index.ts
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
  }
}
 
// index.js
class Square {
  constructor(width) {
    this.width = width
  }
}
class Rectangle extends Square {
  constructor(width, height) {
    super(width)
    this.width = width
    this.height = height
  }
}
function calculateArea(shape) {
  if (shape instanceof Rectangle) {
    return shape.width * shape.height
  } else {
    return shape.width * shape.width
  }
}

인터페이스는 타입으로만 사용가능하지만, 클래스로 선언하면 타입과 값 모두 사용할 수 있게된다.

type Shape = Square | Rectangle에서 Rectangle는 타입으로 참조되지만, shape instanceof Rectangle는 값으로 참조된다.

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

// bad
// index.ts
function asNumber(val: number | string): number {
  return val as number
}
 
// index.js
function asNumber(val) {
  return val
}
 
// good
// index.ts
function asNumber(val: number | string): number {
  return typeof val === 'string' ? Number(val) : val
}
 
// index.js
function asNumber(val) {
  return typeof val === 'string' ? Number(val) : val
}

위의 첫번째 코드의경우 타입체커는 통과하지만 잘못된 방법이다. 자바스크립트로 컴파일해보면 코드에 아무런 정제과정이 없는것을 확인할 수 있다. as number는 타입단언문이고 런타임에는 아무런 영향을 미치지 않는다.

값을 정제하기 위해서는 런타임 타입을 체크해야하고 자바스크립트 연산을 통해 변환을 수행해야 한다.

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

아래 코드에서 console.log는 실행될 것인가??

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn()
      break
    case false:
      turnLightOff()
      break
    default:
      console.log('실행되지 않을까 봐 걱정된다.')
  }
}

매개변수타입이 boolean이기 때문에 console.log는 실행되지 않을거 같다...

타입스크립트는 일반적으로 실행되지 못하는 죽은코드를 찾아내지만 여기서는 strict을 설정하더라도 찾아내지 못한다..

왜 찾아내지 못할까..?????

: boolean는 타입스크립트의 타입이기 떄문에 런타임에 제거된다. 자바스크립트였다면 실수로 setLightSwitch("ON")과 같은 형식으로 호출할 수도 있다.

타입스크립트에서도 마지막코드를 실행하는 방법이 존재한다.

예를들어, 네트워크 호출로 부터 받아온 값으로 함수를 실행하는 경우가 있다.

interface LightApiResponse {
  lightSwitchValue: boolean
}
 
async function setLight() {
  const response = await fetch('/light')
  const result: LightApiResponse = await response.json()
  setLightSwitch(result.lightSwitchValue)
}

/light를 요청하면 그 결과로 LightApiResponse를 반환하라고 선언했지만, 실제로 그렇게 되리라는 보장은 없다.

API를 잘못 파악해서 lightSwitchValue가 문자열이라면, 런타임에서는 setLightSwitch함수까지 전달 될 것이다.

타입스크립트에서는 런타임 타입과 선언된 타입이 맞지 않을 수 있기 때문에, 선언된 타입이 언제든지 달라질 수 있다는 것을 명심해야 한다.

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

C++같은 언어는 동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용하지만, 타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에, 함수 오버로딩은 불가능하다.

// index.ts
function add(a: number, b: number) {
  return a + b
} // Error: 중복된 함수 구현이다.
function add(a: string, b: string) {
  return a + b
} // Error: 중복된 함수 구현이다.
 
// index.js
function add(a, b) {
  return a + b
}
function add(a, b) {
  return a + b
}

타입스크립트가 함수 오버로딩 기능을 지원하기는 하지만, 타입수준에서만 동작한다.

하나의 함수에 대해 여러개의 선언문을 작성할 수 있지만 구현체는 하나뿐이다.

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

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

타입스크립트는 '런타임' 오버헤드가 없는 대신, '빌드타임' 오버헤드가 있다. 오버헤드가 커지면 빌드도구 설정을 통해서 타입체크를 건너뛸 수도 있다.

📕 요약

  • 코드 생성은 타입 시스템과 무관하다. 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않는다.
  • 타입 오류가 존재하더라도 컴파일은 가능하다.
  • 타입스크립트 타입은 런타임에 사용할 수 없다.

📋 Item 4. 구조적 타이핑에 익숙해지기

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

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

타입스크립트는 이를 모델링 하기 위해 구조적 타이핑을 사용한다.

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

💡 구조적 타이핑(structural typing)이란?
코드 구조 관점에서 타입이 서로 호환되는지의 여부를 판단하는 것

interface Vector2D {
  x: number
  y: number
}
 
interface NamedVector {
  name: string
  x: number
  y: number
}
 
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y)
}
 
const v: NamedVector = { x: 3, y: 4, name: 'Zee' }
calculateLength(v) // OK

위의 예제를 보면 calculateLength함수의 매개변수로 Vector2D타입을 사용해야하는 함수이다.

하지만 NamedVectorVector2D타입의 관계를 선언하지 않았는데 오류가 나지 않는 것을 확인할 수 있다.

타입스크립트는 자바스크립트의 런타임 동작을 모델링하고, NamedVector구조가 Vector2D와 호환되기 때문에, calculateLength함수의 호출이 가능하다.

하지만, 구조적 타이핑 때문에 문제가 발생하기도 하지만, 구조적 타이핑을 제대로 이해 하고 있다면 오류인 경우와 오류가 아닌 경우의 차이를 파악할 수 있고, 더욱 견고한 코드를 작성할 수 있다.

📕 요약

  • 자바스크립트가 덕 타이핑 기반이고 타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용하고 있다.
  • 구조적 타이핑을 사용하면 유닛테스팅을 쉽게 할 수 있다.

📋 Item 5. any 타입 지양하기

  • any타입에는 타입 안정성이 없다. age는 number타입으로 선언되었는데 as any를 사용하면 string타입을 할당 할 수 있게된다..
let age: number = 12
age = '12' // Error: string형식을 number타입에 할당할 수 없다.
age = '12' as any // OK
  • any는 함수 시그니처를 무시해 버린다. birthDatestring이 아닌 Date타입이어야 하지만, any타입을 사용하면 함수 시그니처를 무시해 문제를 일으킬 수 있다.
function calculateAge(birthDate: Date): number {
  //...
  return 0
}
 
let birthDate1 = '1990-01-01'
calculateAge(birthDate1) // Error: string 유형의 인수를 Date 유형의 파라미터에 할당 할 수 없다.
 
let birthDate2: any = '1990-01-01'
calculateAge(birthDate2) // OK
  • any타입에는 언어서비스가 적용되지 않는다. 타입스크립트 언어서비스는 자동완성 기능과 도움말을 제공해준다. 그러나 any타입을 사용하게되면 이러한 도움을 받을 수 없게된다.

  • any타입은 코드 리팩터링 때 버그를 감춘다.

  • any는 타입 설계를 감춰버린다. 애플리케이션 상태같은 객체를 정의하려면 상태 객체 안에 있는 수많은 속성의 타입을 작성해야 하는데, any타입을 사용하면 간단하게 끝낼 수도 있다. 하지만 any타입을 사용하면 타입설계가 불분명 해지기 때문에 설계가 명확히 보이도록 타입을 일일이 작성하는게 좋다.

  • any는 타입시스템의 신뢰도를 떨어뜨린다. 타입체커가 사람의 실수를 줄여주고 코드의 신뢰도를 높이는데, any타입을 사용하지 않으면 런타임에 발견될 오류를 미리 잡을 수 있고 신뢰도를 높일 수 있다. any타입을 많이 사용하게되면 자바스크립트로 작성하는것 보다 일을 더 어렵게 만들기도 한다..

📕 요약

  • any타입을 사용하면 타입체커와 타입스크립트 언어 서비스를 무력화시키기 때문에, 타입시스템의 신뢰도를 떨어뜨린다. 최대한 사용하지말자..

📚 출처

profile

Park Cheol Hwi

Copyright ©2024 kcjfgnl9205 All rights reserved.