[이펙티브 타입스크립트] 아이템 01, 02, 03

다시보려고 쓰기·2022년 11월 19일
0
post-thumbnail

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

타입스크립트와 자바스크립트의 관계

  • 타입스크립트는 자바스크립트의 상위집합
  • 모든 자바스크립트는 타입스크립트이지만, 모든 타입스크립트가 자바스크립트는 아니다.
  • 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것.
  • 타입 체커를 통과한 자바스크립트 프로그램을 타입스크립트 프로그램이라고 할 수 있다.



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

타입스크립트 noImplicitAny, strictNullChecks 설정

noImplicitAny

noImplicitAny는 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어한다.
function add(a, b) {
  // 매개변수에는 암시적으로 'any' 형식이 포함된다.
  return a + b;
}

새 프로젝트를 시작한다면 처음부터 noImplicitAny를 설정하여 코드를 작성할 때마다 타입을 명시하도록 해야한다.
그러면 타입스크립트가 문제를 발견하기 수월해지고, 코드의 가독성이 좋아지면, 개발자의 생산성이 향상된다.
noImplicitAny 설정 해제는 자바스크립트로 되어 있는 기존 프로젝트를 타입스크립트로 전환하는 상황에만 필요하다.

strictNullChecks

strictNullChecks는 null과 undefined가 모든 타입에서 허용되는지 확인하는 설정이다.
const x: number = null; 
// 에러 [Type 'null' is not assignable to type 'number']

const el = document.getElementById('status');
el.textContent = 'Ready'; 
// 에러 [Object is possibly 'null']

if (el) {
  el.textContent = 'Ready'; 
  // null 체크 로직 추가
}
// 또는
el!.textContent = 'React'; 
// el이 null이 아님을 단언하는 (!)표기 추가

strictNullChecks는 null과 undefined 관련된 오류를 잡아내는 데 많은 도움이 되지만 코드 작성을 어렵게 한다.

새 프로젝트를 시작한다면 가급적 strictNullChecks를 설정하는 것이 좋지만, 타입스크립트가 처음이거나 자바스크립트 코드를 마이그레이션 하는 중이라면 설정하지 않아도 괜찮다.

프로젝트가 거대해질수록 설정 변경은 어려워질 것이므로, 가능한 한 초반에 설정하는게 좋다.
모든 체크를 설정하고 싶다면 strict 설정을 하면 된다. (엄격한 체크)



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

큰 그림에서 보면 타입스크립트 컴파일러는 두 가지 역할을 수행한다.
(이 두 가지는 완전히 독립적)

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

  • 타입 오류가 있어도 타입 체크 기능과 트랜스파일 기능은 독립적으로 동작하기 때문에 컴파일이 가능하다.

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

instanceof 체크 구문은 런타임에 실행되지만 Rectangle은 타입이기 때문에 런타임에서는 아무런 역할을 할 수가 없다.
실제로 자바스크립트로 컴파일되는 과정에서 모든 인터페이스 타입 구문은 그냥 제거된다.

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;
  }
}
런타임에 정보를 유지하는 방법은 실제 속성이 존재하는지 체크해 보는 것이다.
// 실제 타입이 존재하는지 체크
function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape; // 타입이 Rectangle
    return shape.width * shape.height;
  } else {
    shape; // 타입이 Square
    return shape.width * shape.width;
  }
}

런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 태그(Tag) 기법을 사용할 수도 있다.

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; // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape; // Type is Square
    return shape.width * shape.width;
  }
}

또 다른 방법으로는 타입과 값으로 모두 사용하기 위해 클래스로 만들어서 사용하는 방법도 있다.

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

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

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

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

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

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

네트워크 호출로부터 받아온 값으로 함수를 실행하는 경우에 선언된 타입과 다른 값이 전달될 수 있다.
타입스크립트에서는 런타임 타입과 선언된 타입이 맞지 않을 수 있으니 타입이 달라지는 혼란스러운 상황을 가능한 피해야 한다.

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      //...
      break;
    case false:
      //...
      break;
    default:
      console.log('실행될 것인가?');
  }
}

interface LightApiResponse {
  lightSwitchValue: boolean;
}
async function setLight() {
  const response = await fetch('/light');
  const result: LightApiResponse = await response.json(); 
  // boolean이 아닐 수 있다!
  setLightSwitch(result.lightSwitchValue);
}

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

타입스크립트는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩이 불가능하다.
하나의 함수에 대해 여러 개의 선언문을 작성할 수 있지만 구현체(implementation)는 오직 하나이다.

// 선언부
function add(a: number, b: number): number;
function add(a: string, b: string): string;

// 컴파일 후
function add(a, b) {
  // 단 한개의 구현체
  return a + b;
}

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

타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에, 런타임의 성능에 아무런 영향을 주지 못한다.
단, 런타임 오버헤드가 없는 대신 타입스크립트 컴파일러는 빌드타임 오버헤드가 있다. 하지만 컴파일러는 매우 빠르고 증분(incremental) 빌드 시 더욱 빠르다.

타입스크립트가 컴파일하여 생성하는 자바스크립트를 오래된 런타임 환경을 지원하기 위해 호환성을 높이고 성능 오버헤드를 감안할지, 호환성을 포기하고 성능 중심 네이티브 구현체를 선택할지 문제에 마주할 수 있다.
호환성을 높여 헬퍼 코드가 추가되면 오버헤드가 생길수 있지만, 이것은 언어 레벨의 문제이고 타입과는 전혀 무관하다.

0개의 댓글