[5장] any 다루기
이펙티브 타입스크립트 5장을 정리한 내용입니다.
작성일 2024.02.14
페이지가 생성된 시간 2024.10.01 21:01:06

0

5장 any 다루기

JS를 TS로 마이그레이션 할 때 any타입이 중요한 역할을 한다.

목표

  • any의 남용을 줄이고 현명하게 사용하는 방법을 익힌다
  • any의 장점은 살리면서 단점은 줄이는 방법을 익힌다

아이템38 any타입은 가능한 한 좁은 범위에서만 사용하기

interface Foo {
  foo: string;
}
interface Bar {
  bar: string;
}
declare function expressionReturningFoo(): Foo;
function processBar(b: Bar) {
  /* ... */
}

// BAD
function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
  return x;
}

// BETTER
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);
  return x;
}

f1의 경우 함수의 마지막까지 x의 타입이 any. 심지어 any타입을 리턴해 프로젝트 전반에 악영향을 끼친다.

f2의 경우 processBar 호출시에만 any. any타입의 사용 범위가 좁다.

요약

  • any의 사용 범위를 최소한으로 줄이기
  • any타입을 반환하지 않기
  • 강제로 타입 오류를 제거하려는 경우 any보다는 @ts-ignore 사용하기

아이템39 any를 구체적으로 변형해서 사용하기

any타입을 그대로 함수에 넣지 않기

// BAD
function getLength(array: any) {
  return array.length;
}

// BETTER
function getLength(array: any[]) {
  return array.length;
}

// 더 나은 3가지 이유
// 1. array.length 타입이 체크 가능
// 2. 함수의 반환 타입이 number
// 3. 함수 호출 시 매개변수가 배열인지 체크 가능

매개변수가 객체이지만 값을 알 수 없다면?

function hasTwelveLetterKey(o: { [key: string]: any }) {
  for (const key in o) {
    if (key.length === 12) {
      return true;
    }
  }
  return false;
}

function hasTwelveLetterKey(o: object) {
  for (const key in o) {
    if (key.length === 12) {
      console.log(key, o[key]);
      //  ~~~~~~ Element implicitly has an 'any' type
      //         because type '{}' has no index signature
      return true;
    }
  }
  return false;
}

object 타입은 객체의 속성 접근이 불가능하다.

함수 타입에서의 any

// BAD
type Fn = any;

// BETTER
type Fn0 = () => any; // 매개변수 없음
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개변수 (Function 타입과 동일)

const numArgsBad = (...args: any) => args.length; // Returns any
const numArgsGood = (...args: any[]) => args.length; // Returns number

요약

any를 더 정확하게 모델링하자. 예를 들면 any[], { [key: string]: any }, () => any


아이템40 함수 안으로 타입 단언문 감추기

타입 정의가 제대로 된 함수 안으로 타입 단언 감추기

declare function shallowEqual(a: any, b: any): boolean;
function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[] | null = null;
  let lastResult: any;
  return function (...args: any[]) {
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}

cacheLast를 호출하는 쪽에서는 any가 사용됐는지 알지 못한다.

타입스크립트의 부족한 문맥 능력을 any 단언으로 보충하기

declare function shallowEqual(a: any, b: any): boolean;
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

k in b를 체크 했으므로 b as any는 안전합니다.

요약

타입 단언문은 일반적으로 위험하지만 상황에 따라 필요하며 현실적인 해결책이 되기도 한다.


아이템41 any의 진화를 이해하기

any 타입의 진화 예시

any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다.

function range(start: number, limit: number) {
  const out = []; // Type is any[]
  for (let i = start; i < limit; i++) {
    out.push(i); // Type of out is any[]
  }
  return out; // Type is number[]
}

const result = []; // Type is any[]
result.push('a');
result; // Type is string[]
result.push(1);
result; // Type is (string | number)[]

any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다.

다음과 같이 함수 호출은 any를 진화시키지 않는다.

// 값을 할당해 any가 진화
function range(start: number, limit: number) {
  const out = []; // Type is any[]
  for (let i = start; i < limit; i++) {
    out.push(i); // Type of out is any[]
  }
  return out; // Type is number[]
}

// 함수 호출로는 any가 진화하지 않는다
function makeSquares(start: number, limit: number) {
  const out = [];
  // ~~~ Variable 'out' implicitly has type 'any[]' in some locations
  range(start, limit).forEach((i) => {
    out.push(i * i);
  });
  return out;
  // ~~~ Variable 'out' implicitly has an 'any[]' type
}

요약

  • 암시적 any, any[] 타입은 진화한다. 이러한 동작을 인지하자.
  • any를 진화시키는 방식보다 명시적 타입 구문을 사용하자.

아이템42 모르는 타입의 값에는 any 대신 unknown을 사용하기

any가 위험한 이유

  • 어떠한 타입이든 any 타입에 할당 가능하다
  • any 타입은 어떠한 타입으로도 할당 가능하다

unknown은 any의 첫 번째 속성만 만족, never는 두 번째 속성만 만족한다

function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml);
}
const book = safeParseYAML(`
  name: Villette
  author: Charlotte Brontë
`) as Book;

// 반환값이 Book이라고 기대하며 함수를 호출하므로 단언문은 문제가 되지 않음

unknown 타입을 좁히기 위해서는 상당히 많은 노력이 필요하다.

in 연사자를 쓰기 전에 객체임을 확인해야 하며 typeof null이 object이므로 별도로 null이 아님을 확인해야 한다.

unknown을 좁히는 예시

// instanceof 사용
function processValue(val: unknown) {
  if (val instanceof Date) {
    val; // Type is Date
  }
}

// 사용자 정의 타입 가드 사용
function isBook(val: unknown): val is Book {
  return (
    typeof val === 'object' && val !== null && 'name' in val && 'author' in val
  );
}

좋지 않은 제너릭 사용의 예

function safeParseYAML<T>(yaml: string): T {
  return parseYAML(yaml);
}

// 제너릭의 사용 스타일이 타입 단언문과 기능적으로 동일하다..
// 이 경우 제너릭보다는 unknown을 반환해 사용자가 직접 단언하거나 타입을 좁히도록 강제할 수 있다.

이중 단언문에서는 any 보다는 unknown을 사용하자

분리되는 즉시 오류를 발생하므로 더 안전하다

let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;

unknown과 비슷하지만 조금 다른 타입들

  • {} 타입은 null과 undefined를 제외한 모든 값을 포함한다
  • object타입은 모든 비기본형(non-primitive) 타입으로 이루어진다

정말로 null과 undefined가 불가능하다고 판단될 때만 {}타입을 사용하자


아이템43 몽키 패치보다는 안전한 타입을 사용하기

몽키패치의 정의가 뭘까?

타입스크립트에서 Document에 임의의 속성 추가하기

// any 단언 사용
(document as any).monky = 'Tamarin';

// 인터페이스 보강 사용
interface Document {
  /** 보강을 사용하면 이렇게 속성에 주석을 붙일 수 있다 */
  monkey: string;
}

document.monkey = 'Tamarin';

보강은 전역적이기 때문에 어떤 경우에는 속성이 있고, 어떤 경우에는 속성이 없는 경우에는 문제가 된다.

이러한 이유로 속성을 monkey: string | undefined와 같이 선언할 수 있지만, 다루기에 불편하다

구체적인 타입 단언문을 사용해 임의의 속성 추가하기

interface MonkeyDocument extends Document {
  monkey: string;
}

(document as MonkeyDocument).monkey = 'Macaque';

Document 타입을 건드리지 않고 확장했으므로, 모듈 영역 문제를 해결할 수 있다.

하지만 몽키패치를 남용하지 말고 궁극적으로는 더 좋은 구조로 리팩터링 하는 것이 좋다.


아이템44 타입 커버리지를 추적하여 타입 안전성 유지하기

noImplicitAny를 설정해도 프로그램 내에 any가 존재할 수 있다

  • 명시적 any 타입: any[]{[key: string]: any}같은 타입은 인덱스를 생성하면 단순 any가 되고 코드 전반에 영향을 미친다.
  • 서드파티 타입 선언: @types 선언 파일을 통해 any타입이 전파되니 조심해야 한다.

npx type-coverage 패키지를 통해 any를 추적할 수 있다

npx type-coverage --detail을 통해 any타입이 있는 곳을 모두 출력해볼 수 있다.

any가 등장하는 몇 가지 문제 살펴보기

  • 명시적 any를 사용한 경우

    • 타입을 정의하고 반환하는 값을 개선해도 명시적으로 any를 반환하므로 모든 타입 정보를 날리게 된다.
    • 반환 타입의 any도 지워줘야 문제가 해결된다.
    function getColumnInfo(name: string): any {
      return utils.buildColumnInfo(appState.dataSchema, name);
    }
    
  • 서드파티 라이브러리로부터 비롯된 any

    • 타입선언이 any를 양산할 수 있다.
    • 서드파티의 타입 선언이 잘못될 수 있다.
©2024 dlwl98
github
PostsAbout