이펙티브 타입스크립트 5장

정태호·2023년 8월 22일
0

타입스크립트

목록 보기
10/13
post-thumbnail

any 다루기

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

function f1() {
  const x: any = expressReturingFoo();
  processBar(x);
}

변수 x의 any 타입이 processBar 호출 이후에도 영향이 미치게 된다.

만약 f1함수에서 x를 리턴할 경우, x에 대한 타입 체크가 되지 않기 때문에 문제가 전염병처럼 퍼지게 된다.

function f1() {
  const x = expressReturingFoo();
  processBar(x as any);
}

any 타입이 processBar 함수의 매개변수에서만 사용된다. 즉, any 타입이 함수 바깥으로 영향을 미치지 않는다.

함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있기 때문에 추론이 가능하더라도 명시하는 것이 좋다.

function f2() {
  const x = expressReturingFoo();
  // @ts-ignore
  processBar(x);
}

@ts-ignore를 사용하면 다음 줄의 오류가 무시되기는 하지만 근본적인 원인을 해결한 것이 아니기 때문에 근본적인 원인을 찾아 적극적으로 대처하는 것이 좋다.

요약

  • any의 사용 범위를 최소한으로 좁혀야 한다.
  • 함수의 반환 타입으로 any를 반환하면 절대 안된다.

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

function getLengthBad(array: any) {
  return array.length;
}

function getLengthGood(array: any[]) {
  return array.length;
}

any가 아닌 any[]를 사용했을 때 이점

  • 함수 내의 array.length 타입이 체크된다.
  • 함수의 반환 타입이 any가 아니라 number로 추론된다.
  • 함수가 호출될 때 매개변수가 배열인지 체크된다.
function hasTweleveLetterKey(o: {[key: string]: any}) {
  for(const key in o) {
    if(key.length === 12) {
      return true;
    }
  }
  return false;
}

객체이긴 하지만 값을 알 수 없다면 [key: string] : any로 선언

함수 타입에 any 사용 시 최소한으로 구체화 하는 세 가지 방법

type Fn0 = () => any;
type Fn1 = (arg: any) => any;
type Fn2 = (...args: any[]) => any;

요약

  • any보다 더 정확하게 모델링 할 수 있도록 any[], {[key: string] : any}, () => any 처럼 구체적인 형태를 사용하자!

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

모든 함수를 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 타입 정보를 힘들게 구성할 필요는 없다.

함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 게 좋은 설계이다.

const 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; // 단언문 추가
};

위 코드에서 단언문을 추가하지 않으면 타입스크립트는 반환문에 있는 함수와 원본 함수 T 타입이 어떤 관련이 있는지 모르기 때문에 오류가 발생한다.

함수 내부에 any가 많이 보이지만 타입 정의에는 any가 없기 때문에 cacheLast를 호출하는 쪽에서는 any가 사용됐는지 알지 못한다.

shallowEqual은 두 개의 매개변수를 받아서 비교하는 함수. 객체를 매개변수로 한다면 타입 정의는 간단하지만 구현이 복잡하다.

객체일 때

const shallowEqual = <T extends object>(a: T, b: T): boolean => {
  for (const [key, value] of Object.entries(a)) {
    // 'string' 형식의 식을 '{}' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
    if (!(key in b) || value !== b[key]) {
      return false;
    }
  }

  return Object.keys(a).length === Object.keys(b).length;
};

// 개선한 코드
if (!(key in b) || value !== (b as any)[key]) {
      return false;
}

b as any 단언문은 앞서 체크를 했으므로 안전하며, 결국 정확한 타입으로 정의되고 제대로 구현된 함수가 된다.

요약

  • 타입 단언문을 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨겨 사용하자.

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

타입스크립트에서 변수의 타입은 변수를 선언할 때 결정된다. 그 후 정제될 수 있지만(null 인지 체크), 새로운 값이 추가되도록 확장할 수는 없다.

any타입과 관련해서 예외인 경우가 존재한다.

function range(start: number, limit: number) {
  const out = []; // any[]
  for(let i = start; i < limit; i++) {
    out.push(i); // any[]
  }
  return out; //반환 타입 number[]로 추론됨 
}

out의 타입은 any[]로 선언되었지만 number 타입의 값을 넣는 순간 number[]로 진화한다.

타입의 진화는 타입 좁히기와 다르다. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화한다.

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

let val: any;
if (Math.random() < 0.5) {
  val = /hello/; //any
  val;
} else {
  val = 12; //any
  val;
}
val; //any

타입의 진화는 값을 할당하거나 배열에 요소를 넣은 후에만 일어나기 때문에, 편집기에서는 이상하게 보일 수 있다.

function range(start: number, limit: number) {
  const out = []; //'out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.

  if (start === limit) {
    return out; // 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
  }
  for (let i = start; i < limit; i++) {
    out.push(i);
  }
  return out;
}

any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생하므로 어떤 변수가 암시적 any 상태일 때 값을 읽으려 하면 오류가 발생한다.

🎈 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.

요약

  • any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야 한다.
  • any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.

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

unknown에는 함수의 반환값과 관련된 형태, 변수 선언과 관련된 형태, 단언문과 관련된 형태가 있다.

any가 강력하면서도 위험한 이유

  • 어떠한 타입이든 any 타입에 할당 가능하다.
  • any 타입은 어떠한 타입으로도 할당 가능하다. (never 타입은 예외)

any 대신 unknown을 사용

  • 어떠한 타입이든 unknown에 할당 가능
  • unknown은 오직 unknown과 any에만 할당 가능

unknown 타입인 채로 값을 사용하면 오류가 발생하기 때문에 적절한 타입으로 변환하도록 강제할 수 있다.

타입 단언문이 unknown에서 원하는 타입으로 변환하는 유일한 방법은 아니다.

// instanceof 체크
function processValue(val: unknown) {
  if(val instanceof Date) {
    val // 타입이 Date
  }
}

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

function processValue(val: unknown) {
  if (isBook(val)) {
    val; // 타입이 Book
  }
}

object, {}

  • {} 타입은 null과 undefined를 제외한 모든 값을 포함
  • object 타입은 모든 비기본형 타입으로 이루어진다. (객체, 배열)

unknown 타입이 도입되기 전에는 {}가 일반적으로 사용되었지만 최근에는 드물다. 정말 undefined, null이 불가능하다고 판단되는 경우에만 {}를 사용하자.

요약

  • unknown은 any 대신 사용할 수 있는 안전한 타입. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 unknown을 사용하자.
  • 타입 단언문, 타입 체크를 사용하도록 강제하려면 unknown을 사용.

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

몽키패치 란 프로그래밍에서 런타임 중에 기존의 코드나 라이브러리를 변경하거나 수정하는 기술을 말한다.

window 또는 DOM 노드에 데이터를 추가한다고 가정해보자. 그 데이터는 기본적으로 전역 변수가 되어 함수를 호출할 때마다 부작용을 고려해야 한다.

또 타입 체커는 내장 속성에 대해서는 알고 있지만, 임의로 추가한 속성에 대해서는 알지 못한다.

document.monkey = 'Tamarin'; // 'Document' 형식에 'monkey' 속성이 없습니다.

(document as any).monkey = 'Tamarin' // ok

any 단언문을 사용해 타입 체커는 통과했지만 타입 안전성을 상실하고 언어 서비스를 사용할 수 없다.

최선의 해결책은 document, DOM으로부터 데이터를 분리하는 것이다.

분리가 어렵다면?

interface의 보강 기법 사용하기

interface Document {
  monkey: string;
}

document.monkey = "Tamarin";
  • 더 안전한 타입이 되어 오타나 잘못된 타입의 할당을 오류로 잡을 수 있다.
  • 속성에 주석을 붙이거나 자동완성을 사용할 수 있다.
  • 몽키패치가 어떤 부분에 적용되었는지 기록이 남는다.

주의할 점

  • 전역적으로 적용됨으로 코드나 다른 라이브러리부터 분리할 수 없다.(Scope 주의)
  • 실행되는 동안 속성이 추가된다면, 실행 시점에 보강을 적용할 방법이 없다.

더 구체적인 타입 단언문 사용

interface MonkeyDocument extends Document {
  monkey: string;
}

(document as MonkeyDocument).monkey = "Tamarin";

Document 타입을 건드리지 않고 확장하는 새로운 타입을 도입했기 때문에 모듈 영역 문제도 해결할 수 있다. 할당문의 타입 또한 안전하다.

요약

  • 전역변수나 DOM에 데이터를 저장하지 말고, 분리해서 사용하자.
  • 몽키패치를 남용하지말고 더 잘 설계된 구조로 리팩토링 하는 것이 좋다.

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

noImplicitAny를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제들로부터 안전하다고 할 수 없다.

any 타입이 존재할 수 있는 두 가지 이유

명시적 any 타입

  • any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입이다.

서드파티 타입 선언

@types 선언 파일들로부터 any 타입이 전파될 수 있다.

  • npx type-coverage 를 활용해서 any를 추적할 수 있다.
  • npx type-coverage --detail 을 활용하면 any 타입이 있는 곳을 모두 출력해 준다.

요약

  • noImplicitAny가 설정되어 있어도, any 타입은 코드 내에 여전히 존재할 수 있다.
  • any를 추적함으로써 타입 안정성을 꾸준히 높일 수 있다.
profile
주니어 프론트엔드 개발자가 되고 싶습니다!

0개의 댓글