해당 포스트는
이펙티브 타입스크립트
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 결론
any
의 사용 범위는 최소한으로 좁히기- 반환 타입에 절대
any
사용하지 않기 - 강제로 타입 오류를 제거해야 하는 경우라면
// @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처럼 사용하기
어떤 객체라도 받는 타입을 정하기 위해서는 object
나 index 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 결론
- 반드시
any
가 필요한지 다시 확인하기 - 만약
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 결론
- 타입 단언문을 불가피하게 사용해야 하는 경우에는 함수 내부에 숨겨서 사용하기
📌 Item 41 ( any의 진화를 이해하기 )
noImplicitAny
가true
라고 가정하고 설멍하겠습니다.
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 결론
- 암시적인
any
,any[]
는 진화함 any
를 진화시키기 보다는 명시적인 타입 사용하기
📌 Item 42 ( 모르는 타입의 값에는 any 대신 unknown을 사용하기 )
unknown
은 두 가지 특징이 있습니다.
- 어떤 타입이든 할당이 가능
- 어떤 타입으로도 할당이 불가능
즉, 어떤 타입도 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
: 모든 값을 가질 수 있음{}
:null
과undefined
를 제외한 모든 값을 가질 수 있음object
: 원시 타입을 제외한 모든 타입을 가질 수 있음
가질 수 있는 값의 범위: unknown
> object
> {}
🎊 Item 42 결론
- 어떤 값이 존재하지만 타입을 확신할 수 없을 때
unknown
사용하기 - 사용자에게 타입 체크를 강제하려면
unknown
사용하기 {}
vsobject
vsunknown
의 차이 이해하기
📌 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 결론
- 데이터를 전역적으로 사용하기 보다는 분리하기
- 몽키패치를 한다면 안전한 방식을 사용하기
- 보강의 모듈 영역 문제를 이해해야 함 ( TODO: 이해 못함… 🥲 )
📌 Item 44 ( 타입 커버리지를 추적하여 타입 안정성 유지하기 )
TODO: 나중에 다시 읽고 정리하기… 절반 이후에는 무슨 말인지 정확히 모르겠음
noImplicitAny
를 사용하더라도 모든 any
에 대해서 안전하지는 않습니다.
0️⃣ any가 존재하는 경우
- 명시적으로
any
를 선언하는 경우에는any
를 사용하고,any
가 전염병처럼 퍼질 수 있습니다. - 서드파티 타입 선언
(@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 결론
noImplicitAny
을 사용한다고 항상any
로 부터 안전하지 않습니다.any
를 추적해서 최대한 줄이고, 타입 안정성을 높여야 합니다.
📮 레퍼런스
- « 이펙티브 타입스크립트 5장 » ( 댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021 )
- donggov - 자바스크립트에서 몽키패치
- 1-blue -
interface
의 보강