이펙티브 타입스크립트 5장 ( Item 38 ~ 44 )
포스트
취소

이펙티브 타입스크립트 5장 ( Item 38 ~ 44 )

해당 포스트는 이펙티브 타입스크립트 5장을 읽고 정리한 포스트입니다.
책의 모든 내용을 작성하는 것이 아닌 주관적인 기준에 따라 필요한 정보만 정리했습니다.

📖 5장 any 다루기

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

any는 사용하지 않거나 가능한 좁은 범위로 사용하는 것이 좋습니다.

any는 타입 체커의 동작을 억제시키기 때문에 정상적으로 타입을 체크하지 않아서 TypeScript의 사용하는 이유중 하나인 런타임 이전에 정적으로 타입을 체크하는 기능을 활용할 수 없게 됩니다.
( 즉, 런타임에 발생할 오류를 미리 확인할 수 없게 됩니다. )

0️⃣ 함수에서 any 타입 좁게 사용하기

함수에서 any를 사용한다면 최대한 좁게 그리고 반환 타입에는 any를 사용하지 않도록 해야합니다.
any를 반환하면 외부의 코드에 any가 퍼지기 때문에 타입 체커의 이점을 활용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
type Foo = { name: string };
type Bar = { age: number };

declare function expressionReturnFoo(): Foo;
declare function processBar(b: Bar): void;

const f1 = () => {
  const x = expressionReturnFoo();

  processBar(x); // Error: 'Foo' 형식의 인수는 'Bar' 형식의 매개 변수에 할당될 수 없습니다.

  return x;
};

const f2 = () => {
  const x: any = expressionReturnFoo();

  processBar(x);

  // any 반환 ( 위험한 행동 )
  return x;
};

const f3 = () => {
  const x = expressionReturnFoo();

  processBar(x as any);

  return x;
};

const f4 = () => {
  const x = expressionReturnFoo();

  // @ts-ignore
  processBar(x);

  return x;
};

const g = () => {
  const foo2 = f2();
  // any를 반환하기 때문에 외부에도 영향을 끼쳐서 타입 체커가 제대로 동작하지 않음
  foo2.fooMethod(); // 에러가 발생되지 않음

  const foo3 = f3();
  // 내부적으로만 any를 사용하기 때문에 외부에 영향을 끼치지 않음
  foo3.fooMethod(); // Error: 'Foo' 형식에 'fooMethod' 속성이 없습니다.
};

1️⃣ 객체에서 any 타입 좁게 사용하기

객체에서 any를 사용할 때도 전체에 사용하기 보다는 필요한 부분에만 사용하는 것이 좋습니다.
잘못 사용하면 객체 전체의 타입 체크를 무시하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type Foo = { name: string };
type Config = {
  a: number;
  b: number;
  c: { key: Foo };
};

const config1: Config = {
  a: 1,
  b: 2,
  c: {
    key: 123, // Error: number' 형식은 'Foo' 형식에 할당할 수 없습니다.
  },
};

const config2: Config = {
  // a, b의 타입 체크를 하지 않음
  a: true,
  b: "문자",
  c: {
    key: 123, // 에러는 사라졌지만 모든 타입 체크를 무시함
  },
} as any;

const config3: Config = {
  // a, b의 타입 체크는 유효함
  a: 1,
  b: 2,
  c: {
    key: 123 as any,
  },
};

🎊 Item 38 결론

  1. any의 사용 범위는 최소한으로 좁히기
  2. 반환 타입에 절대 any사용하지 않기
  3. 강제로 타입 오류를 제거해야 하는 경우라면 // @ts-ignore 사용하기

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

any는 그 어떤 타입도 받을 수 있기 때문에 만약 any를 사용한다고 하더라도 최대한 구체적으로 표현하는 것이 좋습니다.

0️⃣ 조금이라도 더 구체적인 any

길이를 구하는 목적이라면 (1)보다는 (2)가 더 구체적입니다.
( (2)의 반환 값은 number로 추론이 됩니다. )

그리고 (3)으로 사용하면 더 구체적이라고 합니다.
그냥 제일 좋은 방법은 any를 사용하지 않는 것이 좋지 않을까 합니다.

만약 필요하다면 unknown을 사용하는 것이 좋은 것 같습니다.
( unknown은 모든 값을 받을 수 있지만 사용하는 경우 타입 에러를 발생시킴 )
TODO: unknown 경로 추가

1
2
3
4
5
6
7
8
9
10
11
12
// (1) (arr: any) => any
const getLengthBad = (arr: any) => arr.length;

// (2) (arr: any[]) => number
const getLength = (arr: any[]) => arr.length;

// (3) 아래 방법이 조금이라도 더 구체적으로 사용하는 방법이라고 합니다.
type F0 = () => any;
type F1 = (arg: any) => any;
type F2 = (...args: any[]) => any; // "Function"타입과 같은 타입
// 추측이긴 한데 "function"타입이 아닌 이유는 "function" 키워드와 겹쳐서 그런 것 같음
// "String"래퍼 객체 -> "string"타입과 같은 느낌

1️⃣ 객체를 any처럼 사용하기

어떤 객체라도 받는 타입을 정하기 위해서는 objectindex signature를 사용해야 합니다. ( 물론 any도 가능하긴 합니다. )

object타입은 열거만 가능하고 프로퍼티에 접근하는 것이 불가능합니다.
하지만 index signature는 모두 가능합니다.
( 그래도 (4)를 통해서 확인했는데 접근이 불가능 한것은 조금 이상하게 느껴지네요.. 🥲 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// "any" 타입 사용
const hasTwelveLetterKey0 = (obj: any) => {
  for (const key in obj) {
    if (key.length === 12) return true;

    // 정상 동작
    console.log(obj[key]);
  }

  return false;
};

// "object" 타입 사용
const hasTwelveLetterKey1 = (obj: object) => {
  // (4)
  for (const key in obj) {
    if (key.length === 12) return true;

    // "object" 타입은 나열만 가능 ( 이상하게 "in"연산자로 확인해도 접근 불가능 )
    console.log(obj[key]); // Error: 'string' 형식의 식을 '{}' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
  }

  return false;
};

// 인덱스 시그니처 사용
const hasTwelveLetterKey2 = (obj: { [key: string]: any }) => {
  for (const key in obj) {
    if (key.length === 12) return true;

    // 정상 동작
    console.log(obj[key]);
  }

  return false;
};

🎊 Item 39 결론

  1. 반드시 any가 필요한지 다시 확인하기
  2. 만약 any를 사용하게 되면 최대한 구체적인 형태로 사용하기

📌 Item 40 ( 함수 안으로 타입 단언문 감추기 )

0️⃣ 함수 내부에서만 확실하게 타입 단언하기

아래 예시는 객체가 같은 지 비교하는 함수입니다.

(1)에서 해당 객체에 key가 존재하는지 확인했지만 b[key]를 사용하면 타입 에러가 발생합니다. ( TypeScript의 문제인 것 같음 )
따라서 앞의 코드로 오류가 날 수 없는 코드임을 확신했기 때문에 (b as any)[key]를 사용했습니다.
( any를 사용하는 타입 단언문의 좋은 예시인 것 같습니다. )

1
2
3
4
5
6
7
8
9
10
const shallowEqual = <T extends object>(a: T, b: T): boolean => {
  for (const [key, value] of Object.entries(a)) {
    // (1) "b[key]"처럼 사용하면 타입 오류 발생 ( 'string' 형식의 식을 '{}' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 있습니다. )
    if (!(key in b) || value !== (b as any)[key]) {
      return false;
    }
  }

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

1️⃣ 함수 내부에서 any 사용과 타입 단언하기

아래 코드는 마지막 호출을 캐싱하는 기능을 하는 함수입니다.

(2)에서는 강제적으로 타입을 변환했는데 그 이유가 타입 체커가 (3)에서 반환하는 함수와 T타입의 관계를 파악하지 못하기 때문입니다.
저희가 코드를 읽으면 어차피 인자로 받은 함수를 호출한 리턴 값을 반환하는 함수를 반환하기 때문에 T를 반환하는 것에 캐싱 기능을 추가한 것이라고 판단할 수 있지만 타입 체커가 판단하지 못하기 때문에 직접적으로 (3)의 반환 함수가 T와 같은 타입이라고 명시했습니다.

그리고 cacheLast() 내부적으로 any를 많이 사용했습니다.
왜냐하면 어떤 함수가 인자로 들어올지 예측할 수 없고 그 함수가 어떤 값을 반환하고 우리가 어떤 값을 캐싱해야 할지 모든 경우를 예측해서 작성할 수 없어서라고 생각합니다.
따라서 내부적으로 any를 이용했지만, 중요한 것은 외부에 any가 나가지 않았기 때문에 내부 동작에만 문제가 없다면 아래 코드는 문제가 발생하지 않는 코드라고 생각했습니다.

아래 예시는 함수 내부에서 any를 활용하고 타입 단언문을 사용하는 것의 좋은 예시인 것 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const cacheLast = <T extends Function>(fn: T): T => {
  let lastArgs: any[] | null = null;
  let lastResult: any;

  // (3)
  return ((...args: any[]) => {
    // 이전 실행과 동일하지 않다면 함수 실행
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    // 그게 아니면 이전의 값을 그대로 사용

    return lastResult;
  }) as unknown as T;
  // (2) "as unknown as T"
};

🎊 Item 40 결론

  1. 타입 단언문을 불가피하게 사용해야 하는 경우에는 함수 내부에 숨겨서 사용하기

📌 Item 41 ( any의 진화를 이해하기 )

noImplicitAnytrue라고 가정하고 설멍하겠습니다.

0️⃣ any의 진화

any 타입의 진화는 암시적인 any인 변수에 다른 값을 할당할 때 발생합니다.

변수 선언에서 초기화하지 않거나 배열 선언에서 빈 배열을 선언하면 any, any[]로 추론됩니다. ( 암시적 )
변수 선언 시 직접적으로 any를 할당하면 명시적인 any이기 때문에 진화가 발생하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// () => (string | number)[]
const func = () => {
  const out = []; // any[]

  out.push(1);
  out; // number[]

  out.push("1");
  out; // (number | string)[]

  return out;
};

const func2 = () => {
  let v; // any

  if (Math.random() > 0.5) {
    v = /^1/;
  } else {
    v = 1;
  }

  // number | RegExp
  return v;
};

const func3 = () => {
  let v: any; // any

  if (Math.random() > 0.5) {
    v = /^1/;
  } else {
    v = 1;
  }

  // any
  return v;
};

1️⃣ 암시적 any 사용

암시적 any를 사용하는 경우 타입 에러가 발생합니다.
또한 배열 고차함수 메서드들을 이용해서는 타입 추론에 영향을 끼치지 못합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const func4 = () => {
  // Error: 'arr' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
  let arr = [];

  // Error: 'arr' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
  arr[0];

  // Error: 'arr' 변수에는 암시적으로 'any[]' 형식이 포함됩니다
  return arr;
};

const func5 = () => {
  // Error: 'arr' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
  let arr = [];

  // 배열 메서드는 영향을 미치지 않음
  [1, 2, 3].forEach((v) => {
    arr.push(v);
  });

  // Error: 'arr' 변수에는 암시적으로 'any[]' 형식이 포함됩니다
  return arr;
};

🎊 Item 41 결론

  1. 암시적인 any, any[]는 진화함
  2. any를 진화시키기 보다는 명시적인 타입 사용하기

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

unknown은 두 가지 특징이 있습니다.

  1. 어떤 타입이든 할당이 가능
  2. 어떤 타입으로도 할당이 불가능

즉, 어떤 타입도 unknown에 넣을 수 있지만, 어떤 타입으로도 사용할 수 없습니다.

0️⃣ any vs unknown

any는 어떤 타입도 받을 수 있고, 어떤 타입으로도 사용할 수 있습니다.
따라서 사용하는 경우에 문제가 발생합니다.
타입 체커가 any는 검사를 하지 않습니다.

하지만 unknown을 사용하기 위해서는 타입을 변환시켜줘야 합니다.
따라서 개발자에 의해서 적절한 타입으로 강제로 변환시켜서 사용하기 때문에 비교적 안전합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Book = {
  title: string;
  author: string;
};

declare function parseYAML1(yaml: string): any;
declare function parseYAML2(yaml: string): unknown;

const book1 = parseYAML1("");
const book2 = parseYAML2("");

book1.title; // 문제 없이 실행

book2.title; // Error: 'book2'은(는) 'unknown' 형식입니다.
(book2 as Book).title; // 강제로 타입 변환 후 실행

1️⃣ unknown 사용 예시

어떤 값이 존재하지만 그 값이 구체적으로 무엇인지 알 수 없는 경우에 unknown을 사용합니다.

그리고 강제로 타입 변환을 하지 않아도 타입 체커에게 unknown이 무엇인지 알려주는 방법이 몇 가지 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Book {
  name: string;
}
declare function isBook(book: unknown): book is Book;
declare class Book {}

const func = (v: unknown) => {
  // (1)
  if (v instanceof Book) {
    v.name; // v: Book
  }

  // (2)
  if (isBook(v)) {
    v.name; // v: Book
  }

  // (3)
  (v as Book).name;
};

2️⃣ {} vs object vs unknown

  • unknown: 모든 값을 가질 수 있음
  • {}: nullundefined를 제외한 모든 값을 가질 수 있음
  • object: 원시 타입을 제외한 모든 타입을 가질 수 있음

가질 수 있는 값의 범위: unknown > object > {}

🎊 Item 42 결론

  1. 어떤 값이 존재하지만 타입을 확신할 수 없을 때 unknown 사용하기
  2. 사용자에게 타입 체크를 강제하려면 unknown 사용하기
  3. {} vs object vs unknown의 차이 이해하기

📌 Item 43 ( 몽키 패치보다는 안전한 타입 사용하기 )

몽키 패치에 대해서는 해당 블로그를 읽어보시기를 추천드립니다!

JavaScript는 굉장히 유연한 언어라서 이미 정의된 객체, 클래스, 함수에도 속성을 마음대로 추가할 수 있습니다.

아래와 같이 이미 만들어져서 제공된 Array 생성자 함수의 prototype에 메서드를 추가하면 기존 배열에서도 추가한 메서드를 그대로 사용할 수 있습니다.
( 물론 이렇게 추가하는 방식은 권장하지 않는 방식입니다. )

1
2
3
Array.prototype.oddFilter = () => {/* ... */};

const odd = [1,2,3,4,5].oddFilter();

0️⃣ 몽키 패치 사용 예시

강제로 any로 변환시켜서 넣을 수 있습니다.

1
2
3
4
5
6
7
// Error: 'Document' 형식에 'monkey' 속성이 없습니다.
document.monkey = "m";

// (1) 정상 동작
(document as any).monkey = "m";
// 아래와 같은 오타를 잡아주지 못함
(document as any).monky = "m";

혹은 interface의 보강을 이용해서 더 안전하게 사용할 수 있습니다.

1
2
3
4
5
6
7
// (2) 보강
interface Document {
  monkey: string;
}
document.monkey = "m"; // 정상 동작
// 아래와 같은 오타를 타입 체커가 잡음
(document as any).monky = "m"; // Error: 'monky' 속성이 'Document' 형식에 없습니다. 'monkey'을(를) 사용하시겠습니까?

아래처럼 구체적인 타입을 새로 만들어서 타입 단언을 사용하는 것도 좋은 방법입니다.

1
2
3
4
5
// (3) 구체적인 타입 단언
interface MonkeyDocument extends Document {
  monkey: string;
}
(document as MonkeyDocument).monkey = "m"; // 정상 동작

그냥 개인적인 생각으로는 굳이 몽키패치를 사용할 이유가 없는 것 같습니다.

🎊 Item 43 결론

  1. 데이터를 전역적으로 사용하기 보다는 분리하기
  2. 몽키패치를 한다면 안전한 방식을 사용하기
  3. 보강의 모듈 영역 문제를 이해해야 함 ( TODO: 이해 못함… 🥲 )

📌 Item 44 ( 타입 커버리지를 추적하여 타입 안정성 유지하기 )

TODO: 나중에 다시 읽고 정리하기… 절반 이후에는 무슨 말인지 정확히 모르겠음

noImplicitAny를 사용하더라도 모든 any에 대해서 안전하지는 않습니다.

0️⃣ any가 존재하는 경우

  1. 명시적으로 any를 선언하는 경우에는 any를 사용하고, any가 전염병처럼 퍼질 수 있습니다.
  2. 서드파티 타입 선언
    ( @type으로 부터 any가 전파될 수 있습니다. )

1️⃣ any 추적 방법

type-coverage를 이용해서 any를 추적할 수 있습니다.

1
2
3
4
5
6
7
8
# 설치
npm i -D type-coverage

# 실행
npx type-coverage

# 상세한 any 위치 파악
npx type-coverage --detail

🎊 Item 44 결론

  1. noImplicitAny을 사용한다고 항상 any로 부터 안전하지 않습니다.
  2. any를 추적해서 최대한 줄이고, 타입 안정성을 높여야 합니다.

📮 레퍼런스

  1. « 이펙티브 타입스크립트 5장 » ( 댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021 )
  2. donggov - 자바스크립트에서 몽키패치
  3. 1-blue - interface의 보강
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

SOP와 CORS와 Preflight와 Credentials

자바스크립트 완벽 가이드 10장 정리 ( module )