타입스크립트의 타입 시스템은 선택적(optional)이고 점진적(gradual)이다. 즉 다른 강타입 언어들과 달리 타입 시스템을 적용할지 안 할지 선택할 수 있고, 프로그램의 일부분에만 타입 시스템을 적용했다가 점점 적용범위를 늘려갈 수도 있다. 따라서 점진적인 마이그래이션(JavaScript → TypeScript)이 가능하다.
이때 any 타입은 타입 체크를 비활성화 시키는 등 타입 시스템에서 벗어나게 해주는 특징이 있기 때문에 마이그레이션에서 중요한 역할을 한다.
//any 는 타입 스크립트의 보호장치(타입 체커)을 무력화 시킨다.
const a: any[] = [1, 2, 3];
const b: any = true;
a + b; //에러가 발생하지 않음!
5장에서는 any 타입을 남용하지 않고 잘 활용하는 방법을 알려주고 있다.
아이템 38. any 타입은 가능한 한 좁은 범위에서만 사용하기
요약
- 의도치 않은 타입 안정성의 손실을 피하기 위해서 any의 사용 범위를 최소한으로 좁혀야 한다.
- 함수의 반환 타입이 any인 경우 타입 안정성이 나빠지므로 any 타입을 반환하면 절대 안 된다.
- 강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는게 좋다.
설명
-
any의 사용 범위를 최소한으로 좁히자.
- any 타입이 강력한 이유
- 어떠한 타입이든 any 타입에 할당 가능하다.
- any 타입은 어떠한 타입에도 할당 가능하다.
- 이러한 특징 때문에 any를 사용하면 타입 에러가 안난다. 이는 any의 강력함의 원천이면서 동시에 문제 원인이 되므로 any가 영향을 미치는 범위를 좁게 만들어 줘야 한다.
- any 타입이 필요할 경우, 변수 타입에 any를 명시하지 말고, 함수 인자로 넘길 때만 타입 단언을 사용하자.
type Foo = { foo: string; }; type Bar = { bar: string; }; const expressionReturningFoo = (): Foo => { return { foo: "foo", }; }; const processBar = (x: Bar): void => { console.log(x); }; function f1() { const x: any = expressionReturningFoo(); //❌ processBar(x); //any 타입이 된 변수 x가 다른 코드에도 영향을 미친다. //또한 이 x가 return 된다면 any 타입이 코드 여기저기 퍼져버리는 상황이 생기게 된다. } function f2() { const x = expressionReturningFoo(); processBar(x as any); //better! ✅ //processBar 함수의 매개변수에만 사용된 표현식이므로 다른 코드에 영향X }
- 객체에서 내부 속성에서 타입 에러가 나면 그 객체 전체가 아니라 에러 나는 부분에만 any 타입 단언을 사용하자.
interface Config { a: number, b: number, c: string } const value = "hi"; const config: Config = { a: 1, b: 2, c: { key: value } } as any; //❌ const config: Config = { a: 1, b: 2, c: { key: value } as any; //✅ }
- any 타입이 강력한 이유
-
함수의 반환 타입이 any인 경우 타입 안정성이 나빠지므로 any 타입을 반환하면 절대 안 된다.
- 함수가 any를 반환하면 그 영향력은 프로젝트 전반에 전염병처럼 퍼지게 된다.
function f3() { const x: any = expressionReturningFoo(); processBar(x); return x; //❌ //any 타입을 절대 반환하지 말자! } function g() { const foo = f3(); //타입이 any }
- any 타입이 생각없이 반환되는 것을 막기 위해 함수의 반환 타입을 명시하자. → any 타입 반환 방지
function f3(): string { //반환 타입 명시 const x: any = expressionReturningFoo(); processBar(x); return x; } function g() { const foo = f3(); //타입이 string }
-
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는게 좋다.
- @ts-ignore : 다음 줄의 오류 무시.
- 해당 타입 오류를 잠깐 건너뛰고 싶을 때 any 로 바꿔주는 것보다는 @ts-ignore가 낫지만, 어디까지나 임시방편임으로 에러가 생기면 적극적으로 대처하는게 바람직하다.
function f4() { const x = expressionReturningFoo(); //@ts-ignore processBar(x); //에러 무시 return x; }
아이템 39. any를 구체적으로 변형해서 사용하기
요약
- any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
- any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.
설명
-
any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
-
any 타입은 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘레먼트, null, undefined 까지 포함한다.
⇒ 일반적인 상황에서 any 보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다.
const numArgsBad = (...args: any) => args.length; //❌ return 타입이 any const numArgsGood = (...args: any[]) => args.length; //✅ return 타입이 number /** 후자가 더 좋은 이유 1. args.length 타입이 체크 됨 2. 함수 반환 타입이 number 로 추론 됨 3. 함수 호출 시, 매개변수가 배열인지 체크 됨 */
-
-
any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다. (그냥 any 보다는 조금이라도 더 구체화시키려 노력하자.)
아이템 40. 함수 안으로 타입 단언문 감추기
요약
- 타입 단언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다. 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.
설명
-
타입 단언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다. 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.
- 가능한 안전한 타입으로 구현하는 것이 이상적이나, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
- 함수 내부에서는 타입 단언 을 사용하고, 함수 외부로 드러나는 타입 정의를 명시하는 정도가 적절할 수 있다.
//예제 - 함수 캐싱하는 함수: 함수가 자신의 마지막 호출을 캐시(기억)하도록 만들어주는 함수 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[]) { if (!lastArgs || !shallowEqual(lastArgs, args)) { lastResult = fn(...args); lastArgs = args; } return lastResult; } as unknown as T; } //원본 함수 타입 T와 리턴하는 함수가 어떤 관련이 있는지 모르기 때문에 에러 발생! //그려나 우리는 두 함수가 '같은 매개변수를 주면 같은 값을 반환하는' 함수여서 //동일하게 취급해도 문제 없다는 것을 알기때문에 단언문 사용해도 괜찮다.
아이템 41. any의 진화를 이해하기
요약
- 일반적인 타입들은 정제(타입좁히기 item22 와 같은 동작)되기만 하는 반면, 암시적 any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야 한다.
- any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.
설명
-
일반적인 타입들은 정제(타입좁히기와 같은 동작)되기만 하는 반면, 암시적 any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야 한다.
- ❗️타입의 진화는 값을 할당하거나 배열의 요소를 넣은 ‘후’에만 일어나기때문에, 편집기에서는 할당 다음 줄을 봐야 진화된 타입이 잡힌다.
//1. 배열의 any 타입 진화(evolve) const result = []; //any[] result.push('a'); result //string[] result.push(1); result //(string | number)[]
//2. 조건문에 따른 any 타입 진화(evolve) let val; //any if(Math.random() < 0.5) { val = /hello/; val //RegExp } else { val = 12; val //number } val //number | RegExp
//3. 초깃값이 null 인 경우 any 타입 진화(evolve) let tmp = null; //any try { somethingDangerous(); tmp = 12; tmp //number } catch (e) { console.log('err!'); } tmp //number | null
- any 타입의 진화는
"noImplicitAny": true
로 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. → 명시적인 경우 진화가 일어나지 않는다.
//명시적인 경우 진화가 일어나지 않는다. let val2: any; //any if(Math.random() < 0.5) { val2 = /hello/; val2 //any } else { val2 = 12; val2 //any } val2 //any
-
any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.
- 암시적 any 상태인 변수에 어떠한 할당 없이 사용하려 하면 암시적 any 오류가 발생한다.
function range(start: number, limit: number) { const out = []; //암시적 any if (start === limit) { return out; //❌ Error:'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다. } for(let i = start; i < limit; i++) { out.push(i); } return out; // number[] 로 추론되기 때문에 에러 없음. }
- 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
function makeSqure(start: number, limit: number) { const out = []; //❌ ''out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다. range(start, limit).forEach(i => { out.push(i * i); }) return out; }
- 의도치 않은 타입이 섞여서 잘못 진화할 수 있기 때문에, (암시적 any 진화 방식보단) 명시적 타입 구문 사용이 더 좋은 설계다.
const result = []; //any[] result.push(1); result //number[] //보다는 처음부터 명시 const result: number[] = [];
아이템 42. 모르는 타입의 값에는 any 대신 unknown을 사용하기
요약
- unknown은 any 대신 사용할 수 있는 안전한 타입이다. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
- unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다. unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
- {} 타입은 null과 undefinedfmf 제외한 모든 값을 포함한다.
설명
-
어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 any 대신 unknown을 사용하자.
- unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
- unknown 은 unknown 인 채로 사용하면 오류가 발생한다.
interface Book { name: string, author: string } //❌ 반환값이 any 여서 타입 체크가 안되어 런타임 에러가 발생한다. const parseYamL = (yaml: string): any => {}; const book = parseYamL(`name: Jane author: Char`); //parseYamL를 호출한 곳에서 타입 선언이나 타입 단언을 하지 않으면 book이 any가 된다. //따라서 아래 코드는 타입 에러도 나지 않고 런타임에서 에러가 난다. console.log(book.title); book(); //✅ 대신 unknown을 쓰자. const safetyParseYamL = (text: string): unknown => ({}); const book2 = safetyParseYamL(`name: Jane author: Char`); //unknown은 타입에러가 난다! 런타임 단계가 아니라 컴파일 단계에서 에러 확인 가능하다. console.log(book2.title); // 'book2'은(는) 'unknown' 형식입니다. book2(); // 'book2'은(는) 'unknown' 형식입니다.
- 이중 단언문에서도 any 대신 unknown 을 사용할 수도 있다.
declare const foo: Foo; let barAny = foo as any as Bar; let barUnk = foo as unknown as Bar;
-
unkown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다.
- 타입의 값을 집합으로 생각하기(아이템7)를 참고해보자.
- any 타입은 모든 타입에 할당 될 수도 있고, 모든 타입이 any에 할당 될 수 있으니, 타입 시스템의 열외로 본다. 하지만 unknown은 타입 시스템에 부합한다.
- any를 제외한 일반적인 타입은 타입 좁히기(더 작은 집합 되기)만 가능하다. 따라서 unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
let num: number; function somethingDo(): unknown { return; } num = somethingDo(); //❌ 'unknown' 형식은 'number' 형식에 할당할 수 없습니다. function hello(): never { throw new Error("xxx"); } num = hello(); //✅ let imNever = hello(); imNever = 12; //❌ 'number' 형식은 'never' 형식에 할당할 수 없습니다
-
{} 타입은 null과 undefined를 제외한 모든 값을 포함한다.
- unknown 타입이 도입되기 이전에는 {}가 일반적으로 사용되었다. 최근에는 잘 사용하지 않는다.
- 정말로 null과 undefined가 불가능하다고 판단되는 경우에만 {}를 대신 사용해야 한다.
- object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다. ex) 객체, 배열
아이템 43. 몽키 패치보다는 안전한 타입 사용하기
요약
- 몽키패치란, 원래 소스코드를 변경하지 않고 실행 시 코드 기본 동작을 추가, 변경 또는 억제하는 기술을 의미한다. 자바스크립트에 있어서는 프로토타입에 특정 메소드 추가 한다거나 document 객체에 전역 변수를 삽입하는 등이 있다.
- 전역 변수나 DOM에 데이터를 저장하는 것보다 데이터를 분리하여 사용해야 한다.
- 어쩔수 없이 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다
설명
-
내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다
//보강 interface Document { monkey: string; } document.monkey = 'Tamarin'; //정상 //사용자 정의 인터페이스로 단언 interface MonkeyDocument extends Document { monkey: string; } (document as MonkeyDocument).monkey = 'Tamarin'; //정상
아이템 44. 타입 커버리지를 추적하여 타입 안전성 유지하기
요약
- noImplicitAny로 암묵적인 any타입 사용을 금지 시켜도, 명식적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있다는 점을 주의하자.
- 작성한 프로그램 타입을 추적해 any 사용을 줄여나가자.
npx type-coverage
를 사용해 프로젝트 심벌중 any 가 아닌 타입의 퍼센트를 확인할 수 있다.npx type-coverage --detail
를 사용해 any 타입이 있는 곳을 모두 출력할 수 있다.