Dev/etc.

이펙티브 타입스크립트 아이템 51 ~ 62

takeU 2023. 6. 29. 11:21
반응형

아이템 51 - 의존성 분리를 위해 미러 타입 사용하기

  • 각자 필요한 모듈만 사용할 수 있도록 구조적 타이핑을 적용
  • 즉, 의존성을 분리해 사용자가 사용에 용이하도록 함
  • 작성중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것(미러링)을 고려해볼 수 있음
    • 다른 라이브러리의 타입 선언 대부분을 추출해야 한다면, 차라리 명시적으로 @types 의존성을 추가하는 것이 나음
    • 유닛 테스트와 상용 시스템 간의 의존성을 분리하는데도 유용

아이템 52 - 테스팅 타입의 함정에 주의하기

헬퍼 함수를 통한 테스팅

// lodash의 map
import { map } from "lodash";

// 매개변수, 반환 타입 모두 체크 가능
function assertType<T>(x: T) {}

assertType<number[]>(map(["john", "paul"], (name) => name.length));

타입을 테스트할 때는 특히 함수 타입의 동일성과 할당 가능성의 차이점을 알고 있어야 함 (구조적 타이핑)

const n = 12;
assertType<number>(n); // 정상
const beatles = ["john", "paul", "george", "ringo"];
assertType<{ name: string }[]>(
  map(beatles, (name) => ({
    name,
    inYellowSubmarine: name === "ringo",
  }))
); // 정상

콜백이 있는 함수를 테스트할 때 콜백 매개변수의 추론된 타입을 체크해야 함

// 선언된 것보다 적은 매개변수를 가진 함수를 할당하는 것은 아무런 문제가 없다는 것을 보여줌
const g: (x: string, y: number) => any = (z: string) => 12; //정상

const g: (x: string) => any = () => 12; //정상

// 로대시의 map함수의 콜백 매개변수 세 개를 모두 사용하는 경우는 매우 드묾
map(array, (name, index, array) => {
  /* ... */
});

제대로 된 assertType 사용 방법

// Parameters와 ReturnType 제네릭 타입을 이용
const double = (x: number) => 2 * x;
let p: Parameters<typeof double> = null!;
assertType<[number, number]>(p); // 오류
// ~ '[number]' 형식의 인수는 '[number, number]'
// 형식의 매개변수에 할당될 수 없음
let r: ReturnType<typeof double> = null!;
assertType<number>(r); // 정상

this가 API의 일부분이라면 역시 테스트해야 함

const beatles = ["john", "paul", "george", "ringo"];
assertType<number[]>(
  map(beatles, function (name, i, array) {
    // ~~~ '(name: any, i: any, array: any) => any' 형식의 인수는
    // '(u: string) => any' 형식의 매개변수에 할당될 수 없음
    assertType<string>(name);
    assertType<number>(i);
    assertType<string[]>(array);
    assertType<string[]>(this);
    // ~~~ 'this'에는 암시적으로 'any' 형식이 포함
    return name.length;
  })
);

declare function map<U, V>(
  array: U[],
  fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];

타입 관련된 테스트에서 any를 주의해야하고, 더 엄격한 테스트를 위해 dtslint같은 도구를 활용하는 것이 좋음

  • 타입 시스템 내에서 암시적 any 타입을 발견해 내는 것은 매우 어려움
  • dtslint 는 할당 가능성이 아닌 심벌 타입을 추출해 글자 자체가 같은지 비교
const beatles = ["join", "paul", "george", "ringo"];
map(
  beatles,
  function (
    name, // $ExpectType string
    i, // $ExpectType number
    array // $ExpectType string[]
  ) {
    this; // $ExpectType string[]
    return name.length;
  }
); // $ExcpectType number[]

아이템 53 - 타입스크립트 기능보다는 ECMAScript 기능을 사용하기

  • 타입 스크립트 팀은 TC39는 런타임 기능을 발전시키고, 타입스크립트 팀은 타입 기능만 발전시킨다는 명확한 원칙을 세우고 지켜오고 있음
  • 해당 원착이 세워지기 전에, 이미 사용되고 있던 몇 가지 기능이 있는데, 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋음

피해야하는 기능

  • 열거형(enum)
    • 숫자 열거형은 할당한 숫자 이외의 숫자가 할당되면 매우 위험
    • 상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거됨
    • preserveConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지함
    • 문자열 열거형은 런타임의 타입 안정성과 투명성을 제공하나 구조적 타이핑이 아닌 명목적 타이핑을 사용
    • 문자열 열거형 대신 리터럴 타입의 유니온 사용
  • 매개변수 속성
    • 컴파일을 하면 코드가 늘어남
    • 런타임에는 실제로 사용되지만, 타입스크립트 관점에서는 사용되지 않는 것 처럼 보임
    • 일반 속성과 섞어 사용하면 설계가 혼란스러워짐
  • 네임스페이스와 트리플 슬래시 임포트
    • module 사용 금지
  • 데코레이터
    • 표준이 아니기 때문에 사용하지 않는 것이 좋음
    • experimentalDecorators 속성을 작성하고 사용해야 함

아이템 54 - 객체를 순회하는 노하우

const obj = {
  one: 'uno',
  two: 'dos',
  three: 'tres'
}

for (const k in obj) {
  const v = obj[k]; // obj에 인덱스 시그니처가 없기 때문에, 암시적 'any' 타입
}

let k: keyof typeof obj;
for (k in obj) {
  const v = obj[k] // 해결
}
  • 타입 문제 없이 객체의 키와 값을 순회하고 싶을 때
interface ABC {
  a: string;
  b: string;
  c: number;
}

function foo(abc: ABC) {
  for (const [k, v] of Object.entries(abc)) {
    k // string 타입
    v // any 타입
  }
}
  • 객체를 다룰 때 '프로토타입 오염'의 가능성을 염두에 두어야 함
  • for in 구문을 사용하면, 객체의 정의에 없는 속성이 등장할 수 있음

결론

  • 객체를 순회하며 키와 값을 얻는 방법
    • let k: keyof T와 같은 keyof 선언 - 상수나 정확한 타입에 적절
    • Object.entries 사용 - 일반적이나, 키와 값의 타입을 다루기 까다로움

아이템 55 - DOM 계층 구조 이해하기

계층 구조별 타입

  1. EventTarget
    • DOM 타입 중 가장 추상화된 타입
    • 이벤트 리스너를 추가, 제거, 이벤트 전송
function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add('dragging') // 개체가 'null'인 것 같습니다.
  // 'EventTarget' 형식에 'classList' 속성이 없습니다.
}
  • Event의 currentTarget 속성의 타입은 EventTarget | null
  • 타입 관점에서 windowXMLHttpRequest가 될 수도 있음
  1. Node 타입
  • 텍스트 조각, 주석
  • children - HTMLCollection
  • childNodes - NodeList, 텍스트 조각과 주석도 포함
  • HTMLxxxElement
    • 자신만의 고유한 속성을 가지기 때문에, 구체적으로 타입을 지정해야 함
    • 단언문을 사용해야 할 경우가 많음
    • null 체크를 해줘야 함

아이템 56 - 정보를 감추는 목적으로 private 사용하지 않기

  • public, protected, private 같은 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 후에 제거됨
  • 즉, 런타임에서 효력이 없음

정보를 숨기는 방법

  • 클로저 사용
  • 비공개 필드 기능 (표준화 진행중)
    • 접두사로 #을 붙여 타입 체크와 런타임 모두에서 비공개로 사용
    • 외부 접근 불가, 개별 인스턴스끼리는 접근 가능
    • WeakMap을 사용해 구현된 기능

아이템 57 - 소스맵을 사용하여 타입스크립트 디버깅하기

  • tsconfig.json에서 sourceMap 옵션 설정을 통해 사용
  • 원본 코드가 아닌 변환된 자바스크립트를 디버깅하는것이 아니라, 소스맵을 사용해 런타임의 타입스크립트 코드를 디버깅해야 함
  • 소스맵이 최종적으로 변환된 코드에 완전히 매핑되었는지 확인해야 함
  • 소스맵에 원본 코드가 공개되지 않도록 설정해야 함

아이템 58 - 모던 자바스크립트로 작성하기

  1. ECMAScript 모듈 사용하기
    • commonJS 대신 ESModule (import, export)
  2. 프로토타입 대신 클래스 사용하기
    • 문법이 간결하며 직관적임
  3. var 대신 let/const 사용하기
    • 스코프 문제를 피할 수 있음
    • 호이스팅 문제를 피할 수 있음
  4. for(;;) 대신 for-of 또는 배열 메서드 사용하기
    • for in 문법은 몇 가지 문제점 때문에 사용하지 않는 것이 좋음
  5. 함수 표현식보다 화살표 함수 사용하기
    • noImplicitThis를 설정해 this 바인딩 관련 오류를 표시해주는 것이 좋음
  6. 단축 객체 표현과 구조 분해 할당 사용
  7. 함수 매개변수 기본값 사용하기
  8. 저수준 프로미스나 콜백 대신 async/await 사용하기
  9. 연관 배열에 객체 대신 MapSet 사용하기
  10. 타입스크립트에 use strict 넣지 않기
    • alwaysStrict 사용하기

아이템 59 - 타입스크립트 도입 전에 @ts-checkJSDoc으로 시험해 보기

  • @ts-check 지시자를 사용해 타입 체커가 파일을 분석하고, 발견된 오류를 보고하도록 지시
    • 매우 느슨한 수준으로 타입 체크를 수행함을 유의
  1. 선언되지 않은 전역변수
    • 변수를 제대로 인식할 수 있게 별도로 타입 선언 파일을 만들어야 함
    • 선언 파일을 찾지 못하는 경우 '트리플 슬래시' 참조 /// <reference path="./types.d.ts" />
  2. 알 수 없는 라이브러리
    • 서드파티 라이브러리들의 타입 선언을 활용하여 타입 체크를 시험해 볼 수 있음
  3. DOM 문제
    • 타입 단언문 대체
  4. 부정확한 JSDoc
    • @ts-check와 같이 사용하면 오류가 발생할 수 있음

아이템 60 - allowJS로 타입스크립트와 자바스크립트 같이 사용하기

  • 점진적 마이그레이션을 위해 자바스크립트와 타입스크립트를 동시에 사용할 수 있게 allowJS 컴파일러 옵션을 사용
  • 마이그레이션 작업 전에, 테스트와 빌드 체인에 타입스크립트 적용

아이템 61 - 의존성 관계에 따라 모듈 단위로 전환하기

  • 다른 모듈에 의존하지 않는 최하단 모듈부터 작업을 시작
  • 마이그레이션과 동시에 리팩토링은 지양

전환시 나타나는 오류들

  1. 선언되지 않은 클래스 멤버
    • 자바스크립트는 클래스 멤버 변수를 선언할 필요가 없지만, 타입스크립트에서는 명시적으로 선언해야 함
    • 멤버 변수를 선언하지 않은 클래스가 있는 js파일을 ts파일로 바꾸면, 참조하는 속성마다 오류가 발생
  2. 타입이 바뀌는 값
    • 객체 선언 시 한번에 선언 or 임시방편으로 타입 단언문 사용
    • @ts-check, JSDoc 작동하지 않음

아이템 62 - 마이그레이션의 완성을 위해 noImplicitAny 설정하기

  • noImplicitAny 설정을 활성화해 마이그레이션의 마지막 단계를 진행해야 함
  • 로컬에서부터 타입 오류를 점진적으로 수정해야 함

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