본문 바로가기
Programming/TypeScript

[이펙티브 타입스크립트] 타입스크립트를 알아보자

by 코뮤(commu) 2023. 7. 13.
728x90
반응형

친구랑 같이 하던 사이드 프로젝트가 드디어 끝이 났다.

이제 남는 점심시간이 생겨서 계속 미뤄뒀던 이펙티브 타입스크립트 책을 읽으며 정리하려고 한다.

현재 타입스크립트로 코드를 작성하면서, 이게 맞나? 이게 제일 효율적인 방식인가?

더 나아가 이게 효율적인 방식인가? 에 대한 의문은 항상 가지고 있는 것 같다.

 

질 좋은 코드를 작성하려면, 좋은 교본을 많이 봐둬야한다고 생각한다.

그래서 이 책을 선택했고, 정말 책 이름 그대로 이펙티브하게 타입스크립트를 써보고자 책 스터디를 진행한다.

예제 코드는 아래 깃헙 링크에 차곡차곡 쌓일 예정이다.

 

https://github.com/do-not-do-that/effective-typescript

 

do-not-do-that/effective-typescript

effective-typescript 스터디. Contribute to do-not-do-that/effective-typescript development by creating an account on GitHub.

github.com

 

 

 

 


 

타입스크립트는 파이썬과 같이 인터프리터로 실행되는 것도 아니고, C같이 저수준 언어로 컴파일되는 것도 아니다.

타입스크립트는 자바스크립트로 컴파일되며 실행 역시 자바스크립트로 된다.

 

 

 

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

 

올바른 문법의 자바스크립트 프로그램은 유효한 타입스크립트 프로그램이다.

자바스크립트 파일은 .js 확장자를 사용하고, 타입스크립트 파일은 .ts 확장자를 사용하지만

타입스크립트는 자바스크립트의 상위집합이기 때문에 main.js 라는 파일명을 main.ts 로 바꾼다고 해도

달라지는 것은 없다.

 

이러한 타입스크립트의 특성은 js코드를 ts 코드로 마이그레이션하는데 굉장히 효율적이다.

 

 

"모든 자바스크립트 프로그램은 타입스크립트이다."

 

위 명제는 이다.

 

하지만 그 반대는 성립하지 않는다.

타입스크립트 프로그램이지만 자바스크립트가 아닌 프로그램이 존재한다.

타입스크립트는 타입을 명시하는 추가적인 문법을 가지기 때문이다.

 

 

타입스크립트 타입 시스템은 자바스크립트의 런타임 동작을 '모델링'한다.

 

 

// 정상
const x = 2 + '5';
const y = '2' + 5;

// 오류
const a = null + 2; // 자바스크립트에서는 a 값이 2가 됨.
const b = [] + 2; // 자바스크립트에서는 b 값이 12가 됨.

 

언제 자바스크립트 런타임 동작을 그대로 모델링할지,

또는 추가적인 타입체크를 할지 분명하지 않다면 타입스크립트를 사용해도 되는가에 대한 의문이 들수도 있다.

나름 결론 내리자면, 내 추가 설정에 따라 달린 것이다.

 

 

간단 요약

 

  • 타입스크립트는 자바스크립트의 상위집합이다.
    모든 자바스크립트 프로그램은 이미 타입스크립트 프로그램이다.
  • 타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입 시스템을 가지고 있기 때문에
    런타임 오류를 발생시키는 코드를 찾아내려한다.
  • 타입 시스템은 전반적으로 자바스크립트 동작을 모델링하지만, 잘못된 매개변수 개수로 함수를 호출하는 경우처럼
    자바스크립트에서는 허용되나 타입스크립트에서는 문제가 되는 경우가 있다.
    (이것은 취향차이이다!)

 

 

타입스크립트 설정

 

위에서 내 추가 설정에 따라 이를 "오류"로 인식할 수도, 아닐 수도 있다고 결론지었다.

그렇다면 설정은 어떻게 하는 것일까?

 

타입스크립트 컴파일러는 아주 많은(최소 100개 이상의) 설정을 가지고 있다.

이 설정들은 커맨드 라인에서 사용할 수도 있고, tsconfig.json 설정 파일을 통해서도 가능하다.

 

설정을 제대로 사용하려면, noImplicitAnystrictNullChecks 를 이해해야한다.

 

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

 

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

 

위 예제는 noImplicitAny 가 해제되어있을 때만 유효한 코드이다.

만약 이를 에디터에서 작성하고 a 변수에 포인터를 대면,

아마 타입스크립트는 a 의 타입을 any 라고 추론한 것을 확인할 수 있을 것이다.

 

any 타입을 매개변수에 사용하면 타입체커는 사실상 거의 제 기능을 못한다고 생각해야한다.

따라서 우리는 noImplicitAny 를 설정하고, 해당 코드를 좀 더 분명한 타입으로 사용해야한다.

 

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

 

 

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

 

// strictNullChecks 가 해제되었을 때
const x: number = null; // 정상

// strictNullChecks 가 설정되었을 때
const y: number = null; // 오류

위 코드를 보면 직관적으로 이해가 된다.

 

만약 null 을 허용하고자 한다면 의도를 명시적으로 드러냄으로써 오류를 고칠 수 있다.

 

const x: number | null = null; // 정상

 

 

코드 생성과 타입은 관계가 없다

 

타입스크립트 컴파일러는 크게 두 가지 역할을 수행한다.

 

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

 

개념을 명확히 잡고 가는게 좋겠다.

위 두 가지 역할은 완벽히 독립적이다.

 

타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않는다는 얘기다.

 

 

컴파일은 타입 체크와 독립적으로 동작하기 때문에

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

 

사실 위 문장이 이해가 안가서 여러번 곱씹었다.

아니 내가 예전에 C 공부할 때는 타입체크와 컴파일이 동시에 됐었는데..?

 

오해하지말자.

타입스크립트의 오류는 C나 자바의 경고문구와 비슷한 역할을 한다.

경고 문구는 개발자에게 noti 의 역할만 수행할 뿐 컴파일시에는 아무 영향을 끼치지 않는다.

 

 

 

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

 

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

 

instanceof 체크는 런타입에 일어나지만,

Rectangle 은 타입이기 때문에 런타임 시점에 아무 역할을 할 수 없다.

 

자바스크립트로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 제거된다.

만약 shape 타입을 명확하게 하려면 런타입에 타입 정보를 유지하는 방법이 필요하다.

아래 방법을 참고해보자.

 

 

1. height 속성이 존재하는지 체크해봄

 

interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

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

 

 

2. 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 '태그' 기법

 

interface Square {
    kind: 'square'
  width: number;
}

interface Rectangle extends Square {
    kind: 'rectangle'
  height: number;
}

type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    shape;
    return shape.width * shape.height;
  } else {
    shape;
    return shape.width * shape.width;
  }
}

 

여기서 Shape 타입은 태그된 유니온의 예시이다.

이 기법은 런타임에 타입 정보를 쉽게 유지할 수 있어서 흔하게 볼 수 있는 코드이다.

 

 

3. 타입을 클래스로 만듦

 

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

 

클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다.

type Shape = Square | Rectangle 부분에서 Rectangle 은 타입으로 참조되지만,

shpage instanceof Rectangle 부분에서는 값으로 참조된다.

 

 

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

 

아래 예시는 타입 체커를 통과하지만, 잘못되었다.

 

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


// 자바스크립트로 컴파일된 코드
function asNumber(val) {
	return val;
}

 

as number 는 타입연산이고, 런타임 동작에는 아무런 영향을 끼치지 않음을 확인할 수 있다.

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

 

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

 

 

타입스크립트 타입으로는 함수 오버로드가 불가능하다

 

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

하나의 함수에 대해 여러 개의 선언문은 작성할 수 있음을 기억하자.

 

// 중복된 함수 구현
function add(a: number, b: number) {return a + b;}
function add(a: number, b: string) {return a + b;}


// 하나의 함수에 대해 여러 개의 선언문 작성 가능
function add(a: number, b: number): number;
function add(a: string, b: string): string;

 

 

 

구조적 타이핑

 

  • 자바스크립트는 덕 타이핑 기반이고, 타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용했다.
    어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있을 것이다.
  • 클래스 도한 구조적 타이핑 규칙을 따른다. 클래스의 인스턴스는 예상과 다를 수 있다.
  • 구조적 타이핑을 사용하면 유닛테스팅을 쉽게 할 수 있다.

 

 

any 타입은 지양하자

any 타입을 사용하면 타입 체커와 타입스크립트 언어 서비스를 무력화 시킨다.

타입 시스템 자체의 신뢰도를 떨어뜨리기 때문에, 최대한 사용을 피하도록 하자.

 

 

 

 


 

 

 

오늘은 이렇게 1장의 내용들을 정리해보았다.

자주 언급되지 않은 단어들이 많이 나오니, 직접 써가면서 익숙해져야겠다는 생각이 든다.

728x90
반응형