본문 바로가기

Front-End

이펙티브 타입스크립트 - 1장. 타입스크팁트 알아보기

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

  • 타입스크립트는 자바스크립트의 상위집합
  • 타입스크립트는 자바스크립트에서 보장해주지 못한 런타임 에러를 타입 체커를 통해 컴파일 시점에 발견할 수 있게 해준다. 

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

아래 코드는 타입 체크를 오류 없이 통과를 할까??

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

add(10,null);

 

정답은 없다. 타입스크립트 설정 파일에 따라 다르기 떄문이다.

// tsconfig.json 파일

{
"compilerOptions:" : 
    {
    	"noImplicityAny" : true
    }
}
  • noImplicityAny : 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어
    • 즉, 위의 예제는 타입을 지정하지 않았기 때문에 에러가 난다.
  • strictNullChecks : null 과 undefined가 모든 타입에서 허용되는지 확인되는 설정

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

타입스크립트 컴파일러가 하는 2가지 일

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

이 2가지 일이 독립적으로 동작하기 때문에 코드의 타입오류가 있다고 해서 컴파일이 안되는 것은 아니다. 즉, 타입 체크를 통과하지 못하더라도 배포가 될 수 있다. 이를 방지하기 위해서는 아래와 같은 설정을 tsconfig.json에 해주면 된다.

noEmitOnError 설정

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

interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    return shape.width * shape.height;
  } else {
    return shape.width * shape.width;
  }
}

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

 

클래스로 만들어버리면 타입과 값으로 모두 사용할 수 있으므로 위의 문제는 사라진다.

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) {
    return shape.width * shape.height;
  } else {
    return shape.width * shape.width;
  }
}

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

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

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

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

  • 타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에, 런타임의 성능에 아무런 영향 없음
  • 타입스크립트 컴파일러는 '빌드타임'오버헤드가 있음. 오버헤드가 커지면, 빌드 도구에서 'transpile only'을 설정하여 타입 체크를 건너뛸 수 있음.

 

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

interface Vector2D {
  x: number
  y: number
}

function calcLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y)
}

위와 같은 코드가 있을때 아래 코드는 에러를 발생시킬까?

interface Vector2DWithName extends Vector2D {
  name: string
}

const a: Vector2DWithName = { name: 'hi', x: 5, y: 10 }
calcLength(a) // works fine

interface Vector2DName {
  name: string
  x: number
  y: number
}

const b: Vector2DName = { name: 'hello', x: 10, y: 10 }
calcLength(b) // works fine, too.

Kotlin 같은 언어였다면 아래 함수는 에러가 났을 것이다. 하지만 타입스크립트에서는 '구조적으로' 타입이 맞기만 한다면 이를 허용해준다. Structural typing

 

아래와 같은 경우는 예상하지 못한 에러를 발생시킬 수 있다.

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

function normalize(v: Vector3D) {
    const length = calcLength(v) // z가 고려되지 않음
    return {
        x: v.x / length,
        y: v.y / length,
        z: v.z / length // z의 값이 이상하게 나옴
    }
}

normalize({x:3, y:4, z:5}) // 그러나 에러는 안남

타입스크립트의 타입은 열려있기 때문에 처음 접하게 되면 아래와 같은 실수를 많이 하게 된다.

function calcLengthV1(v: Vector3D) {
  let length = 0
  for (const axis of Object.keys(v)) {
    const coord = v[axis] // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Vector3D'.
    // No index signature with a parameter of type 'string' was found on type 'Vector3D'.(7053)
    length += Math.abs(coord)
  }
  return length
}

즉, 아래와 같은 예제에서 타입이 열려있기 때문에 예상치 못한 에러를 발생시킬 수 있다.

const v = { x: 1, y: 2, z: 3, name: 'hi, h i~' }
calcLengthV1(v) // name의 값이 NaN이라서 결과가 NaN으로 뜰 수 있다.

아래와 같이 작성하여야 의도치 않은 실수를 막을 수 있다.

function calcLengthV2(v: Vector3D) {
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z)
}

 

하지만 구조적 타이핑 덕분에 아래와 같이 편하게 코딩도 가능하다.

interface Employee {
  name: string
  id: number
}

interface DB {
  runQuery: (sql: string) => any[]
}

function getEmployee(db: DB): Employee[] {
  const rows = db.runQuery('SELECT name, id from EMPLOYEES')
  return rows.map((row) => ({ name: row[0], id: row[1] }))
}

 

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

  • 타입스크립트 언어 서비스를 무력화 시킴
  • 리팩토링할때 위험
  • 개발 경험, 신뢰도 뜰어뜨림