TypeScript 5장(38 ~ 42)

이종서·2022년 12월 27일
0

TypeScript

목록 보기
7/9

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

⭐️함수와 관련된 any의 사용법

function processBar(b: Bar) {/* ... */}
function f(){
  const x = expressionReturningFoo();
  processBar(x);
  //        ~~~ 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}

오류를 해결하는 방법

// 1. 좋지 않은 방법
function f1(){
  const x: any = expressionReturningFoo();
  processBar(x);
}

// 2. 나은 방법
function f2(){
  const x = expressionReturningFoo();
  processBar(x as any);
}

두번째 방법을 더 권장합니다.
any 타입이 processBar 함수의 매개변수에서만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않기 때문입니다. f1에서는 함수의 마지막까지 x의 타입이 any인 반면, f2에서는 processBar 호출 이후에 x가 그대로 Foo 타입입니다.

❗️만일 f1 함수가 x를 반환한다면 그 영향력은 프로젝트 전반에 전염병처럼 퍼지게 됩니다.

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

function g() {
  const foo = f1(); // 타입이 any
  foo.fooMethod(); // 이 함수 호출은 체크되지 않습니다!
}

타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋습니다.

❗️@ts-ignore를 사용하면 any를 사용하지 않고 오류를 제거할 수 있지만 근본적인 원인을 해결한 것은 아니기 때문에 다른곳에서 더 큰 문제가 발생할 수 있습니다.

⭐️객체와 관련된 any의 사용법

const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
// ~~~~~ 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
  }
}

단순히 생각하면 config 객체 전체를 as any로 선언해서 오류를 제거할 수 있습니다.

const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
  }
} as any; // 이렇게 하지 맙시다!

이렇게 될 경우 다른 속성들(a와 b) 또한 타입 체크가 되지 않는 부작용이 생깁니다.
최소한의 범위에만 any를 사용하는 것이 좋습니다.

const config: Config = {
  a: 1,
  b: 2, // 이 속성은 여전히 체크됩니다.
  c: {
    key: value as any
  }
}

요약

📌 의도치 않은 타입 안전성의 손실을 피하기 위해서 any의 사용 범위를 최소한으로 좁혀야 합니다.
📌 함수의 반환 타입이 any인 경우 타입 안전성이 나빠집니다. 따라서 any 타입을 반환하면 절대 안 됩니다.
📌 강제로 타입 오류를 제거하라면 any대신 @ts-ignore 를 사용하는 것이 좋습니다.

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

any는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입입니다.
any 타입의 값을 그대로 정규식이나 함수에 넣는 것은 권장하지 않습니다.

// 1번 예제
function getLengthBad(array: any) { // 이렇게 하지 맙시다
  return array.length;
}

// 2번 예제
function getLength(array: any[]) {
  return array.length;
}

1번보다 2번 예제가 더 좋은 함수인 이유

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

함수의 매개변수가 객체이긴 하지만 값을 알수 없다면 {[key: string]: any}처럼 선언하면 됩니다.

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

다른 방식으로는 비기본형 타입을 포함하는 object 타입을 사용할 수도 있습니다.
(object 타입은 객체의 키를 열거할 수는 있지만 속성에 접근할 수 없습니다.)

function hasTwelveLetterKey(o: object) {
  for(const key in o) {
    if (key.length === 12) {
      console.log(key, o[key]);
      //			   ~~~~~~~ '{}' 형식에 인덱스 시그니처가 없으므로
      //			   			요소에 암시적으로 'any' 형식이 있습니다.
      return true;
    }
  }
  return false;
}

❗️ 객체지만 속성에 접근할 수 없어야 한다면 unknown 타입이 필요한 상황일 수 있습니다.

함수의 타입에도 단순히 any를 사용해서는 안됩니다. 최소한으로 구체화 할 수 있는 3가지 방법이 있습니다.

type Fn0 = () => any;	// 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개변수 "Function"타입과 동일합니다.

any로 선언해도 동작하지만 any[]로 선언하면 배열형태라는 것을 알수 있어서 더 구체적입니다.

const numArgBad = (...args: any) => args.length;  // any를 반환
const numArgGood = (...args: any[]) => args.length;  // number를 반환

요약

📌 any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 합니다.
📌 any보다 더 정확하게 모델링할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any 처럼 구체적인 형태를 사용해야 합니다.

Item 40. 함수 안으로 타입 단언문 감추기

제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 좋은 설계입니다.

어떤 함수가 자신의 마지막 호출을 캐시하도록 만든다고 가정

두 개의 배열을 매개변수로 받아서 비교하는 함수 shallowEqual

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[]) {
    //  ~~~~~~~~~~~~~~~~~~~~~~~~~~
    // '(...args: any[]) => any' 형식은 'T'에 할당할 수 없습니다.
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return last REsult;
  };
}

타입스크립트는 반환문에 있는 함수와 원본 함수 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 last REsult;
  } as unkown as T;
}

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

객체를 매개변수로 하는 shallowObjectEqual 구현

declare function shallowObjectEqual<T extends object>(a: T, b: T): boolean;

declare function shallowEqual(a: any, b: any): boolean;
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [km aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {
      					//	 ~~~~~~ '{}' 형식에 인덱스 시그니처가 없으므로
      					//	요소에 암시적으로 'any' 형식이 있습니다.
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

any로 단언해서 오류 해결

function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
  for (const [km 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;
}

타입 단언문은 안전하며, 결국 정확한 타입으로 정의되고 제대로 구현된 함수가 됩니다.
객체가 같은지 체크하기 위해 객체 순회와 단언문이 코드에 직접 들어가는 것보다, 앞의 코드처럼 별도의 함수로 분리해 내는 것이 훨씬 좋은 설계입니다.

요약

📌 타입 선언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 합니다. 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨기도록 합니다.

Item 41. any의 진화를 이해하기

타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정됩니다.
그 후에 정제될 수 있지만, 새로운 값이 추가되도록 확장할 수는 없습니다. 그러나 any 타입과 관련해서 예외인 경우가 존재합니다.

일정 범위의 숫자들을 생성하는 함수

// 자바스크립트
function range(start, limit) {
  const out = [];
  for (let i = start; i < limit; i++) {
    out.push(i);
  }
  return out;
}

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

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

❗️타입의 진화는 타입 좁히기와 다릅니다.

배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화합니다.

const result = []; // 타입이 any[]
result.push('a');
result // 타입이 string[]
result.push(1);
result // 타입이 (string | number)[]

조건문에서는 분기에 따라 타입이 변합니다.

let val; // 타입이 any
if (Math.random() < 0.5) {
  val = /hello/;
  val // 타입이 RegExp
} else {
  val = 12;
  val // 타입이 number
}
val // 타입이 number | RegExp

변수 초기값이 null인 경우도 any의 진화가 일어납니다.
보통은 try/catch 블록 안에서 변수를 할당하는 경우에 나타납니다.

let val = nul; // 타입이 any
try {
  somethingDangerous();
  val = 12;
  val // 타입이 number
} catch (e) {
  console.warn('alas!');
}
val // 타입이 number | null

any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어납니다. 다음처럼 명시적으로 any를 선언하면 타입이 그대로 유지됩니다.

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

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

function makeSquares(start: number, limit: number) {
  const out = [];
  	//	~~~ 'out' 변수는 일부 위치에서 암시적으로 'any[]' 형식입니다.
  range(start, limit).forEach(i => {
    out.push(i * i);
  });
  return out;
  //	~~~~~ 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}

타입을 안전하게 지키기 위해서는 암시적 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계입니다.

요약

📌 일반적인 타입들은 정제되기만 하는 반면, 암시적 any와 any[] 타입은 진화 할 수 있습니다.
📌 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법입니다.

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

1. 함수의 반환값과 관련된 unknown

YAML 파서인 pareseYAML 함수를 작성

function parseYAML(yaml: string): any {
  // ...
}

함수의 반환 타입으로 any를 사용하는 것은 좋지 않은 설계입니다.
대신 parseYAML를 호출한 곳에서 반환값을 원하는 타입으로 할당하는 것이 이상적입니다.

interface Book {
  name: string;
  author: string;
}
const book: Book = parseYAML(`
	name: Wuthering Heights
	author: Emily Bronte
`);

그러나 함수의 반환값에 타입 선언을 강제할 수 없기 때문에, 호출한 곳에서 타입 선언을 생략하게 되면 book 변수는 암시적 any타입이 되고, 사용되는 곳 마다 오류가 발생하게 됩니다.

const book = parseYAML(`
	name: Jane Eyre
	author: Charlotte Bronte
`);
alert(book.title); // 오류 없음, 런타임에 "undefined" 경고
book('read'); // 오류 없음, 런타임에 "TypeError: book은 함수가 아닙니다" 예외 발생

parseYAML이 unknown 타입을 반환하게 만드는 것이 더 안전합니다.

function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml);
}
const book = safeParseYAML(`
	name: The Tenant of Wildfell Hall
	author: Anne Bronte
`);
alert(book.title);
//	~~~~~~~ 개체가 'unknown' 형식입니다.
book("read");
// ~~~~~~~~~~~ 개체가 'unknown' 형식입니다.

any가 강력하면서도 위험한 특징

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

'타입을 값의 집합으로 생각하기'의 관점에서, 한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없기 때문에, 분명히 any는 타입 시스템과 상충되는 면을 가지고 있습니다.

profile
프론트엔드 개발자

0개의 댓글