Dev/etc.

이펙티브 타입스크립트 아이템 1 ~ 10

takeU 2023. 6. 1. 10:10
반응형

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

타입스크립트는 자바스크립트의 상위집합(superset)
'정적' 타입 시스템 - 컴파일 시 변수의 타입을 지정하는 것

타입스크립트의 특징

  • 자바스크립트의 런타임 동작을 모델링 함
    • 타입 시스템은 런타임에 오류를 발생시킬 코드를 미리 찾아 냄
  • 모든 오류를 찾아주지는 않고, 의도와 다르게 동작할 수도 있음
    • 타입 체커를 통과하면서도 런타임 오류를 발생시킬 수 있음

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

tsconfig.json에서 설정
tsc --init으로 설정파일 생성

대표적인 설정값

  • noImplicitAny
    • 변수들이 미리 정의된 타입을 가져야 하는지 여부 제어, Any도 불가능.
    • 타입을 정하지 않았어도 암시적으로 any로 간주하여 오류 발생시킴.
    • 되도록 해당 설정 해야함.
  • strictNullCheckes
    • nullundefined가 모든 타입에서 허용되는지 여부
    • null을 쓰려고 하면 유니온 타입으로 null 명시.
    • 오류 잡는데 많은 도움이 되지만 타입스크립트가 처음이거나 마이그레이션한다면 설정하지 않아도 괜찮음.
    • strict로 엄격한 체크 가능

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

TS 컴파일러의 역할

  • 최신 TS/JS를 구버전 JS로 트랜스파일
  • 타입 오류 체크

이 때 두가지는 독립적으로 작동하기 때문에 다음과 같은 경우가 생길 수 있다.

타입 오류가 있어도 컴파일이 가능

  • 경고를 해주지만 빌드를 멈추지는 않음
  • 즉, 작성한 TS가 유효한 JS라면 컴파일을 정상적으로 실행
  • 오류가 있을 때 컴파일을 멈추려면 noEmitOnError 속성을 추가

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

  • 컴파일되는 과정에서 interface, type, 타입 구문은 제거됨
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
// 런타임 시점에서 타입이 모두 지워져서 Rectangle이 값으로 여기게 됨
    return shape.width * shape.height;
                    //         ~~~~~~ Property 'height' does not exist
                    //                on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}
  • 따라서 타입을 명확하게 하기 위해선 세 가지 방법이 존재함
    • 타입 가드 - 해당 속성이 존재하는지 확인하는 방법으로 구분(if (type in class))
    • 태그 기법 - type Shape = Square | Rectangle과 같이 Union으로 엮어 구분
    • 타입을 클래스로 만들어 interfaceof로 구분
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
  }
}

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

function asNumber(val: number | string): number {
  return val as number;
}
  • as number를 사용해서 타입을 바꾸는 방식은 아무런 영향이 없게 작동됨,
  • as number는 타입연산이고 런타임에서는 동적하지 않음.
function asNumber(val: number | string): number {
  return typeof(val) === 'string' ? Number(val) : val;
}
  • 런타임에서 타입 체크 및 연산을 하기 위해서는 위와 같이 수행 해야함.

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

  • 타입선언문이 런타임에 제거되기 떄문에 여러 코드가 실행되면서 정확한 타입을 보장해주지는 않음.

타입으로는 함수를 오버로드 할 수 없음

  • 타입과 런타임 동작이 무관하기 때문에, 함수 오버로딩이 불가능
  • 같은 이름의 다른 두 개의 선언문은 JS로 변환되면서 제거되고, 구현체만 남게됨

타입은 런타임 성능에 영향을 주지 않음

  • 타입과 타입 연산자는 JS 변환 시점에 제거되기 때문에, 런타임 성능에 영향을 주지 않음
  • 즉, 정적 타입은 비용이 들지 않음
  • 런타임 오버헤드 대신 빌드타임 오버헤드 존재

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

구조적 타이핑이란?

  • 멤버만으로 타입을 관계시키는 방법
  • 즉, 구조가 같으면 같은 타입으로 간주함
  • 타입의 확장에 열려있기 때문에, 같은 속성명을 가지면 다른 속성이 추가로 있는 타입과의 차이를 잡아낼 수 없음

구조적 타이핑의 장점

  • 유닛 테스트에 유용함 (비교가 쉬움)
  • 라이브러리 간의 의존성을 완벽하게 분리할 수 있음

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

  • 타입스크립트는 점진적이고(gradual), 선택적(optional)하다.
    • 타입 추가 가능(gradual)
    • 타입체커를 해제 가능(optional)
  • 이러한 기능들은 any 타입으로 가능하게 됨.

any의 위험성

  1. 타입 안정성 X, ‘as any’와 같이 오류를 해결하기 위해 any를 사용한다면 타입의 혼돈이 생김.
  2. 함수 시그니처 무시, 함수를 호출 및 출력할 때 함수의 약속된 타입을 어기게 됨.
  3. 언어서비스 적용 X, 자동완성 기능과 도움말을 사용할 수 없음.
  4. 리팩토링때 버그를 감춤, any를 통해 타입체커가 통과한다 해도 잘못된 매개변수를 받으면 런타임에 오류각 발생하게 됨.
  5. 타입설계를 감춤, 복잡한 객체 정의를 간단하게 끝내버려 다른 동료들은 타입 설계를 어떻게 했는지 전혀 알수가 없음.
  6. 타입시스템의 신뢰도 저하, any를 통해서 타입 체커가 잡아지지 않으면 타입체커의 신뢰도가 하락되어 개발자가 직접 타입 오류를 고쳐야 함.

아이템 6 - 편집기를 사용하여 타입 시스템 탐색하기

  • 타입 선언 파일 찾아보며 동작 확인

아이템 7 - 타입이 값들의 집합이라고 생각하기

  • 타입 - 할당 가능한 값들의 집합

집합의 크기

  • never - 아무 값도 포함하지 않는 공집합
  • 유닛(Unit), 리터럴 - type A = 'a'와 같이 한 가지 값만 포함
  • 유니온(Union) - 여러개를 묶기 위해 파이프(|) 사용
  • 타입 지정 - string, number와 같이 무한대 범위를 지정
  • unknown - universal set

교집합(&, intersection)

  • 속성에 대해 합집합처럼 생각하면 된다
interface A { a: string }
interface B { b: string }
type C = A & B
const obj: C {
  a: 'a',
  b: 'b'
} // 정상

서브타입(subtype)

interface A extends B

아이템 8 - 타입 공간과 값 공간의 심벌 구분하기

  • 심벌이 타입인지 값인지 구분해야 함

아이템 9 - 타입 단언보다는 타입 선언을 사용하기

  • 변수에 값을 할당하고 타입을 부여하는 두 가지 방법>ㅁ
interface Person { name: string }

const alice: Person = { name: 'Alice' } // 타입 선언
const bob = { name: 'bob' } as Person // 타입 단언
  • 함수 호출 체이닝이 연속되는 곳에서는 시작에서부터 명명된 타입을 가져야 함
  • 타입 단언은 꼭 필요한 경우에 사용해야 함(DOM)
  • !를 통해 null이 아님을 단언할 수 있음 (ex: document.getElementById('foo)!)
  • as unknown을 통해 서로의 서브타입이 아닐 때 변환 가능(지양)

아이템 10 - 객체 래퍼 타입 피하기

  • 객체 래퍼 타입은 지양하고, 기본형 타입을 사용해야 함
    • String > string, Number > number 등..
  • stringString에 할당할 수 있지만, 반대의 경우는 되지 않음.
  • 따라서 타입스크립트에서 객체 래퍼 타입을 지양하고, 기본형 타입을 사용해야 한다.
  • BigInt와 Symbol은 new가 없는 경우 기본형을 생성하여 사용해도 상관없음.
  • typeof BigInt(1234) "bigint" typeof Symbol('sym') "symbol"

출처: 이펙티브 타입스크립트