[이펙티브 타입스크립트] 5장 any 다루기

JIY00N·2023년 8월 22일
0

TypeScript

목록 보기
6/9
post-thumbnail
post-custom-banner

5장 any 다루기

any의 장점은 살리면서 단점을 줄이는 방법들에 대해서 알아보자.

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


1. 함수와 관련된 any

function processBar(b: Bar){ // ... }
function f(){
  const x = expressionReturningFoo();
  processBar(x); // 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다. 
}
  
// 문맥상으로 x라는 변수가 동시에 Foo 타입과 Bar타입에 할당 가능하다면,
// 오류 제거 방법이 두가지 있다.
  
// 1. f1은 함수의 마지막까지 x의 타입이 any -> 다른 코드에 영향 o
function f1(){
  const x: any = expressionReturningFoo(); // 이렇게 하지 맙시다!
  processBar(x);
}
  
// 2. processBar 호출 이후에 x가 그대로 Foo 타입 -> 다른 코드에 영향 x
function f2(){
  const x = expressionReturningFoo();
  processBar(x as any); // 이게 낫습니다.
}
  
// 3. @ts-ignore 사용 -> any를 사용하지 않고 오류를 제거할 수 있지만 근본적인 해결책 x
function f1(){
  const x = expressionReturningFoo();
  // @ts-ignore
  processBar(x);
  return x;
}  
  • 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다.

2. 객체와 관련된 any

// 어떤 큰 객체 안의 한 개 속성이 타입 오류를 가지는 상황
const config: Config = {
  a: 1,
  b: 2, 
  c: {
    key: value
    // 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
  }
};

// 1. config 객체 전체를 as any로 선언해서 오류 제거
// -> a,b 속성 타입 체크 불가능
const config: Config = {
  a: 1,
  b: 2, 
  c: {
    key: value
  }
} as any; // 이렇게 하지 맙시다!

// 2. 최소한의 범위에만 any 사용 -> a,b 속성 타입 체크 가능
const config: Config = {
  a: 1,
  b: 2, 
  c: {
    key: value as any
  }
};

🎯 요약

최소한의 범위에서만 any를 사용하자

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


  • any 보다 더 구체적으로 표현할 수 있는 타입이 있다면, 그것을 쓰자
// 함수 타입에도 단순히 any를 사용해서는 안 된다.
// 최소한으로나마 구체화할 수 있는 세 가지 방법

type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: ang[]) => any; // 모든 개수의 매개변수 "Function" 타입과 동일합니다. 
// (any[] 로 선언하면 배열 형태라는 것을 알 수 있어서 구체적임)

// any[] 타입을 사용하는 가장 일반적인 경우
const numArgsBad = (...args:any) => args.length; // any를 반환
const numArgsGood = (...args: any[]) => args.length; // number를 반환

🎯 요약

최소한의 범위에서만 any를 사용하자

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


  • 프로젝트 전반에 위험한 타입 단언문이 드러나 있는 것보다, 제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계다.
  1. 함수 구현
declare function shallowEqual(a: any, b: any): boolean

// 원본 함수 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가 많이 사용됐지만,
// cahceLast를 호출하는 쪽에서는 any가 사용되었는지 알지 못한다.
  1. 객체 구현 - 객체 매개변수가 동일한 키를 가진다는 보장이 없기 때문에 주의해야 함
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)) {
    // k in b 체크로 b가 k 속성이 있다는 것을 확인했지만, b[k]에서 오류 발생
    // 따라서 b as any 로 단언
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false
    }
  }
  return Object.keys(a).length === Object.keys(b).length
}

🎯 요약

any를 사용해야 한다면, 함수 안으로 숨기자

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


  • 타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정되지만, any는 예외인 경우가 존재한다.
  • 타입의 진화(evolve)가 일어나는 경우

    1. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화함
    2. 조건문에서는 분기에 따라 타입이 변할 수 있음
    3. 변수의 초깃값이 null인 경우
    4. noImplicitAny 가 설정된 상태에서 변수의 타입이 암시적인 any인 경우
      (단, 명시적으로 any를 선언하면 타입이 그대로 유지됨)
    5. 암시적 any 타입에 어떤 값을 할당할 때만 발생하고, 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생
  • 타입의 진화는 타입 좁히기와 다르다.

// 1. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화함
const result = []; // 타입이 any[]
result.push('a');
result; // 타입이 string[]
result.push(1);
result; // 타입이 (string|number)[]
// 3. 변수의 초깃값이 null인 경우도 any의 진화가 일어남(보통 try/catch문)
let val = null; // 타입이 any
try {
  somethingDangerous();
  val = 12;
  val; // 타입이 number
}catch(e){
  console.warn('alas!');
}
val // 타입이 number | null
  • 암시적인 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계이다.

🎯 요약

any와 any[]는 일반적인 타입과 다르게 진화가 가능하므로 동작이 발생하는 코드를 인지하고 이해하자

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


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

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

// 이상적 반환값 타입 할당
function parseYAML(yaml: string): any {
  // ...
}
interface Book {
  name: string
  author: string
}
const book: Book = parseYAML(`
  name: Jane Eyre
  author: Charlotte Brontë
`)


// 1. any 사용
function parseYAML(yaml: string): any {
  // ...
}
interface Book {
  name: string
  author: string
}
// 함수의 반환값에 타입 선언을 강제할 수 없기 때문에,
// 호출한 곳에서 타입 선언 생략하여 book 변수는 암시적 any
// 따라서, 사용되는 곳 마다 에러가 발생한다.
const book = parseYAML(`
  name: Jane Eyre
  author: Charlotte Brontë
`)
alert(book.title) // 오류 없음, 런타임에 "undefined" 경고
book('read') // 오류 없음, 런타임에 "TypeError: book 은 함수가 아닙니다" 예외 발생


// 2. unknown 사용
// any 대신 쓰자 -> any는 타입 시스템과 상충되는 면을 가지고 있음
// (한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없다)
interface Book {
  name: string
  author: string
}
function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml)
}
// unknown 타입인 채로 값을 사용하면 오류가 발생하므로 적절한 타입으로 강제 변환
const book = safeParseYAML(`
  name: Villette
  author: Charlotte Brontë
`) as Book
alert(book.title)
// ~~~~~  'Book' 형식에 'title' 속성이 없습니다.
book('read')
// ~~~~~~~~~ 이 식은 호출할 수 없습니다.

2. 변수 선언과 관련된 unknown

  • 어떠한 값이 있지만 그 타입을 모르는 경우에 unknown 사용

  • unknown 에서 원하는 타입으로 변환하기

  1. 타입 단언문

  2. instanceof로 체크

interface Geometry {}
function processValue(val: unknown) {
  if (val instanceof Date) {
    val // Type is Date
  }
}
  1. 사용자 정의 타입 가드 사용
interface Geometry {}
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 // Type is Book
  }
}
  • 제너릭 보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다.

3. 단언문과 관련된 unknown

interface Foo {
  foo: string
}
interface Bar {
  bar: string
}
declare const foo: Foo
// barAny와 barUnk는 기능적으로 동일하지만,
// 나중에 두 개의 단언문을 분리할 때, unknwon 형태인 barUnk가 더 안전하다.
let barAny = foo as any as Bar 
let barUnk = foo as unknown as Bar // 분리되는 즉시 오류를 발생
  • {}과 object
  1. {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.
  2. object 타입은 모든 비기본형 타입(객체, 배열,함수 등)으로 이루어진다.

🎯 요약

any보다는 unknown이 안전하다.

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


  • 몽키 패치
    프로그래밍에서 특정 클래스나 모듈의 기능을 수정하거나 확장하기 위해 런타임 중에 코드를 동적으로 변경하는 기술을 의미.

window 또는 DOM 노드에 데이터를 추가한다고 가정
-> 데이터는 전역 변수가 되고, 함수를 호출할 때마다 부작용을 고려해야 함

1. 문제점
타입 체커는 Document와 HTMLElement의 내장 속성에 대해서는 알지만, 임의로 추가한 속성(monkey)은 모른다

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

2. 해결책

2-1. any 단언문 사용
단점 - any를 사용해서 타입 안정성이 불안하고, 언어 서비스를 이용하지 못한다.

(document as any).monkey = 'Tamarin' // 정상

(document as any).monky = 'Tamarin' // 정상으로 나오지만 오타 있음
(document as any).monkey = /Tamarin/ // 정상으로 나오지만 잘못된 타입

2-2. Document또는 DOM으로부터 데이터를 분리(최선책)

😑 내장 타입과 데이터 분리가 어려운 상황이라면...?

2-3. interface의 기능인 보강을 사용(차선책)
주의 - 모듈 영역(scope), 보강은 전역적으로 적용되기 때문에 코드의 다른 부분이나 라이브러리부터 분리할 수 없다.
애플리케이션이 실행되는 동안 속성을 할당하면, 실행 시점에서 보강을 적용할 방법이 없다.

interface Document {
  /** 몽키 패치의 속(genus) 또는 종(species) */
  monkey: string
}

document.monkey = 'Tamarin' // 정상


// 모듈의 관점에서 제대로 동작하게 하려면, global 선언을 추가
export {}
declare global {
  interface Document {
    /** 몽키 패치의 속(genus) 또는 종(species) */
    monkey: string
  }
}
document.monkey = 'Tamarin' // 정상

2-4. 더 구체적인 타입 단언문 사용(차선책)

// 1. MonkeyDocument는 Document를 확장하기 떄문에 타입 단언문은 정상이며 할담문의 타입도 안전
// 2. Document 영역을 건드리지 않고 확장하여 새로운 타입을 도임해서 모듈 영역 문제도 해결
interface MonkeyDocument extends Document {
  /** 몽키 패치의 속(genus) 또는 종(species)  */
  monkey: string
}

(document as MonkeyDocument).monkey = 'Macaque'

🎯 요약

몽키 패치보다는 안전한 방법으로 타입을 사용하자

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


  • any 타입이 프로그램내에 존재 할 수 있는 경우

    1. 명시적 any 타입 - any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any
    2. 서드파티 타입 선언 - @types 선언 파일로부터 any 타입이 전파되기 때문에 조심해야 함.
    noImplicitAny를 설정했어도, any 타입은 여전히 코드 전반에 영향을 미침

  • any의 개수를 추적하여 타입 안정성을 높이자

    1. npx type-coverage
      9985 / 10117 98.69%
      -> 프로젝트의 10117개 심벌 중 9985개(98.69%)가 any가 아니거나 any의 별칭이 아닌 타입을 가지고 있다.

    2. npx type-coverage --detail
      -> any 타입이 있는 곳을 모두 출력

🎯 요약

any를 추적하여 타입 안정성을 높이자

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글