[Typescript] any를 현명하게 사용하자

김유진·2023년 5월 16일
0

Effective-TypeScript

목록 보기
19/28
post-thumbnail

타입스크립트를 사용하다 보면 불친절한 라이브러리를 만나거나, 형태가 너무 복잡해서 any를 사용해야 할 때가 존재한다.
하지만, any를 사용해야 한다고 해서 막 쓰는 것이 아니라 더욱 현명하고 안전하게 사용할 수 있다는 점을 이번 Item 38, 39를 통해 배웠기 때문에 이에 대해 기록하고자 한다.

any의 사용 범위를 최소한으로 줄이자.

함수에서의 any 사용

function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
}
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);

f1, f2 두 함수 중에서 더욱 any를 현명하게 사용한 것은 어떤 것일까? 바로 f2 함수이다. 그 이유는 아래와 같다.

  • any 타입이 processBar 함수에 매개변수에만 사용된 표현식이다.
  • f 함수의 반환에서 x 가 반한되었을 때 any가 확장되는 문제점을 최소화할 수 있다.

이렇게 함수가 반환될 때, 되도록 함수 반환 타입은 추론할 수 있는 경우에도 반환 타입을 명확하게 명시하는 것이 좋다. any가 사용될 수 있는 여지를 최소한으로 하자는 것이다.

객체에서의 any사용

프로퍼티 c에 사용되는 것에 대한 타입을 잘 알지 못해 아래와 같이 객체를 작성했다고 가정해보자.

const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
  }
} as any;

객체 전체를 any로 단언하게 되면 다른 속성들도 타입 체크가 되지 않아서 피해가 막심해진다. 그러므로 잘 모르는 것만 딱 체크해서 타입 체크를 해보자.

const config: Config = {
  a: 1,
  b: 2, 
  c: {
    key: value as any
  }
}

any를 최대한 구체적으로 작성하자

any보다는 any[]

any는 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM엘리먼트는 물론 null과 undefined까지 포함한다. any보다는 더욱 구체적으로 표현할 수 있는 타입은 항상 존재하기 때문에, any를 작성하다고 해도 더욱 구체적인 타입으로 세분화하여 작성하는 습관을 들이면 좋다.

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

이렇게 작성하기보다는 아래와 같이 작성하도록 해보자.

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

아래와 같이 작성하는 것이 더욱 좋은 코드이다. 그 이유를 정리해보자.

  • 함수 내의 array.length 타입이 체크된다.
  • 함수의 반환 타입이 any 대신 number로 추론된다.
  • 함수가 호출될 때 매개변수가 배열인지 체크된다.

배열이 아닌 값을 넣어도 제대로 값을 체크 못하는 상황이 발생하기 때문에 같은 any를 쓰더라도 구체화된 값으로 사용하는 것이 낫다는 것이다.

getLangthBad(/123/)

해당 코드가 오류 없이 실행되는 것은 좋은 코드라고 할 수 없을 것이다.

객체의 타입을 잘 모를 때

[key: string]: any

객체에 대해서 코드를 작성할 때, 나도 이렇게 코드를 작성한 적이 몇 번 있다.
CSS모듈이 타입스크립트에서 잘 안통하길래 따로 type이나 인터페이스를 명시해주고 어떤 타입의 style도 받을 수 있도록 하는 것이다.
이는 어떤 변수가 객체이지만, 값을 알 수 없다면 사용하면 되고, 다른 경우에는 Object 타입을 이용하는 것이다. (사실 나는 이걸 더 많이 쓰는 듯?)
Object 타입은 객체의 키를 열거할 수는 있지만, 속성에 접근할 수 없다는 점에서 {[key: string]: any}와 약간 다르다.

더 구체적인 any이용 with 함수

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

같은 ...args라도, 배열로 설정해주냐 아니냐에 따라서 차이가 발생한다.

any를 항상 기피할 필요 없다.

함수의 외부와 내부, any가 하나도 없이 완벽하게 모든 타입을 정의해줄 수만 있다면 정말 좋을 것이다. 하지만, any를 모든 곳에서 완벽하게 사용하기란 정말 어려운 일이다. 이렇게 어려운 일을 별 문제 없이 완료하기 위하여 사용하는 것이 바로, 타입 단언문을 잘 작성된 함수의 타입 안에 감추는 것이다.

이렇게 함수가 가져오는 매개변수와 반환 타입을 아래와 같이 정확히 정의했다고 하자.

declare function cacheLast<T extends Function>(fn: T): T;

그리고 이 함수를 실제로 구현해보자.

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

이렇게 함수를 구현해 볼 수 있는데, 함수 안에 any가 많다고 해서 좋지 않은 코드라고 섣불리 판단하는 것은 올바르지 않다. 타입 단언문을 작성하였다고 하여도 반환 타입을 정확하게 사용하고 있고, 해당 함수 밖으로 잘못 쓰인 any 타입이 방출될 일이 없으므로 단언문을 안에 작성하여 감추는 것이 크게 힘들이지 않고 잘 작성한 코드라고 평가될 수 있는 것이다.

0개의 댓글