Search
Duplicate
📧

5장. any 다루기

전통적으로 프로그래밍 언어들은 타입 시스템이 정적이거나 동적이거나 둘로 확실히 구분되어 있었다.
타입스크립트의 타입 시스템은 선택적이기 때문에 정적이면서도 동적인 특성을 모두 가진다.
덕분에 프로그램의 일부분에만 타입 시스템을 적용하며 마이그레이션이 가능한데, 이때 코드의 일부에 타입 체크를 비활성화하는 any 타입이 중요한 역할을 한다.
이 과정에서 any를 현명하게 사용할 줄 알아야 효과적인 타입스크립트 코드를 작성할 수 있다.

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

먼저 다음 코드를 살펴보자.
function processBar(b: Bar) { /* ... */ } function f() { const x = expressionReturningFoo(); processBar(x); // ~ Argument of type 'Foo' is not assignable to // parameter of type 'Bar' }
TypeScript
복사
x 변수는 expressionReturningFoo() 함수로부터 Foo 타입의 값을 할당받을 수 있으므로 문맥상 Foo 타입과 Bar 타입을 모두 가질 수 있다.
이 내용을 명시해주려면 형변환이 더 낫다.
function f1() { const x: any = expressionReturningFoo(); // Don't do this processBar(x); } function f2() { const x = expressionReturningFoo(); processBar(x as any); // Prefer this }
TypeScript
복사
그 이유는 any 타입이 processBar 함수의 매개변수에만 사용되므로 다른 코드에는 영향을 미치지 않기 때문이다.
f1의 경우 xf1이 종료될 때까지 any인 반면 f2에서는 processBar 호출 이후 x는 여전히 Foo 타입이다.
만약 f1return x를 해버린다면 문제는 전파되어 더 복잡해질 것이다.
비슷한 관점에서 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다.
함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있기 때문이다.
요약
의도치 않은 타입 안전성의 손실을 피하기 위해서 any의 적용 범위를 최소한으로 좁혀야 한다.
함수의 반환 타입이 any인 경우 타입 안전성이 나빠진다. 따라서 any 타입을 반환하는 것은 절대 피해야 한다.
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는 것이 좋다.

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

any 타입에는 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스는 물론 nullundefined도 포함된다.
일반적인 상황에서 any보다 더 구체적으로 표현할 수 있기 때문에 구체적인 타입을 찾아 타입 안전성을 높이도록 해야한다.
다음 예제에서 any를 사용하는 getLengthBad보다는 any[]를 사용하는 getLength가 더 좋은 함수다.
function getLengthBad(array: any) { // Don't do this! return array.length; } function getLength(array: any[]) { return array.length; }
TypeScript
복사
타입 체커는 함수 내의 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; }
TypeScript
복사
물론 원시적이지 않은 타입을 포함하는 모든 경우에 object 타입을 사용할 수 있다. 허나 이는 키를 열거할 수는 있으나 값에 접근할 수 없다.
객체의 키를 열거할 수는 있어야 하지만 속성에 접근할 필요가 없다면 unknown 타입을 사용해도 괜찮다.
요약
any를 사용할 때, 정말 모든 자바스크립트의 값이 할당가능한지 아닌지 생각하라.
정확한 타입을 사용하는 것이 좋다. any[]{[id: string]: any}와 같이 사용하라. 그들은 데이터를 보다 정확하게 모델링한다.

item 40. 잘 타입화된 함수 안으로 안전하지 않은 타입 단언문을 숨기기

함수를 작성하다 보면 외부로 드러난 타입 정의는 간단하지만 내부 구현이 복잡해서 타입 안전성을 확보하기 어려운 경우가 많다.
함수의 모든 부분에서 타입 안전성을 확보할 수 있도록 구현하는 것이 이상적이지만 불필요한 예외 상황까지 고려해야가며 타입 정보를 구성할 필요는 없다.
함수 구현에는 타입 단언을 사용하고 함수 외부로 범위가 확장될 가능성이 있는 부분에 대해서 타입 정의를 정확히 명시하는 정도로 끝내는 게 낫다.
넓은 범위에 타입 단언문을 노출하는 것보다 반환 타입이 제대로 명시된 함수의 좁은 범위의 안에 타입 단언문을 감추는 것이 더 좋은 설계다.
예를 들어 어떤 함수가 자신의 마지막 호출을 캐시하도록 만든다고 가정해보자.
declare function cacheLast<T extends Function>(fn: T): T; function cacheLast<T extends Function>(fn: T): T { let lastArgs: any[]|null = null; let lastResult: any; return function(...args: any[]) { // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // Type '(...args: any[]) => any' is not assignable to type 'T' if (!lastArgs || !shallowEqual(lastArgs, args)) { lastResult = fn(...args); lastArgs = args; } return lastResult; }; }
TypeScript
복사
타입스크립트는 반환문에 있는 함수와 원본 함수 T 타입 선언이 어떤 관련이 있는지 모르기 때문에 오류가 발생한다.
결과적으로 원본 함수 T 타입과 동일한 매개변수로 호출되고 반환값 역시 예상한 결과가 되기 때문에 타입 단언문을 추가해서 오류를 제거하는 것이 큰 문제가 되지는 않는다.
as unknown as T;
실제로 함수를 실행해 보면 잘 동작하는데, 함수 내부에는 any가 꽤 많이 보이지만 타입 정의 자체에는 any가 없기 때문에 cacheLast를 호출하는 쪽에서는 any가 사용됐는지 알지 못한다.
요약
타입 선언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다.
불가피하게 사용해야 한다면 정확한 정의(매개변수, 반환값)를 가지는 함수 안으로 숨기도록 한다.

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

타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정된다. 그 후에도 범위를 좁히는 것은 가능하지만 새로운 값 할당이 가능하도록 확장할 수는 없으나 any 타입을 이용하면 예외적으로 가능해진다.
자바스크립트에서 일정 범위의 숫자들을 생성하는 함수를 예로 들어보겠다.
function range(start, limit) { const out = []; for (let i = start; i < limit; i++) { out.push(i); } return out; }
TypeScript
복사
위 코드를 다음 타입스크립트 코드로 변환하면 예상한대로 정확히 동작한다.
function range(start: number, limit: number) { const out = []; // Type is any[] for (let i = start; i < limit; i++) { out.push(i); // Type of out is any[] } return out; // Type is number[] }
TypeScript
복사
자세히 살펴보면 이상한점이 하나 있음을 알 수 있다. out 변수를 처음 선언할때 any[] 타입으로 초기화하였는데 return의 경우 outnumber[]로 추론된다.
이는 number 타입의 값을 넣는 순간부터 타입이 number[]진화하기 때문이다.
타입의 진화는 타입 좁히기와 다른데, 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되면서 진화한다.
const result = []; // Type is any[] result.push('a'); result // Type is string[] result.push(1); result // Type is (string | number)[]
TypeScript
복사
조건문에서는 분기에 따라 타입이 변할 수도 있다.
let val; // Type is any if (Math.random() < 0.5) { val = /hello/; val // Type is RegExp } else { val = 12; val // Type is number } val // Type is number | RegExp
TypeScript
복사
변수의 초깃값이 null인 경우에도 any의 진화가 일어나는데, 보통은 try/catch 블록 안에서 변수를 할당하는 경우 나타난다.
let val = null; // Type is any try { somethingDangerous(); val = 12; val // Type is number } catch (e) { console.warn('alas!'); } val // Type is number | null
TypeScript
복사
any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. 명시적으로 any를 선언하는 경우 타입이 그대로 유지된다.
any가 진화하는 방식은 일반적인 변수가 추론되는 원리와 동일한데, 진화한 배열의 타입이 (string|number)[]라면, 원래 number[] 타입이 실수로 string이 섞여서 잘못 진화한 것일 수 있다.
타입을 안전하게 지키기 위해서는 암시적 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계다.
요약
일반적인 타입들은 정제되기만 하지만 암시적 anyany[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 이유를 인지하고 알고 있어야 한다.
any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 타입 안전성을 확보하는 방법이다.

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

unknownany 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다.
unknownany의 첫 번째 속성(어떠한 타입이든 할당 가능)을 만족하지만 두 번째 속성(unknown은 오직 unknownany에만 할당 가능)은 만족하지 않는다.
반면 never 타입은 unknown과 정반대다.
한편 unknown 타입인 채로 값을 사용하면 오류가 발생하기 때문에 적절한 타입으로 변환하도록 강제할 수 있다.
const book = safeParseYAML(` name: Villette author: Charlotte Brontë `) as Book; alert(book.title); // ~~~~~ Property 'title' does not exist on type 'Book' book('read'); // ~~~~~~~~~ this expression is not callable
TypeScript
복사
함수의 반환 타입인 unknown 그대로 값을 사용할 수 없기 때문에 Book 타입 단언을 수행해야 한다.
이때 타입 단언 대신 instanceof를 사용해도 원하는 타입으로 변환할 수 있다.
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 } }
TypeScript
복사
가끔 unknown 대신 제네릭 매개변수가 사용되는 경우도 있는데, 일반적으로 이는 타입스크립트에서 좋지 않은 스타일이다.
제네릭을 사용한 스타일은 타입 단언문과 달라보이나 기능적으로 동일하게 동작한다.
제네릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁힐 수 있도록 강제하는 것이 더 낫다.
{}object의 경우 unknown보다 범위가 약간 좁은 타입이다.
{} 타입은 nullundefined를 제외한 모든 값을 포함한다.
object 타입은 일반적으로 모든 비기본형 타입으로 이루어진다.
이때 기본형임에도 객체와 배열은 포함된다.
요약
unknownany 대신 사용할 수 있는 안전한 타입으로, 어떠한 값이 있지만 타입을 확신하지 못하는 경우 사용하라.
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown을 사용하면 된다.
{}, object, unknown의 차이를 인지하고 있어야 한다.

item 43. 몽키 패치보다는 안전한 타입을 사용하기

자바스크립트의 특징 중 하나인 객체와 클래스에 임의의 속성을 추가하는 기능 때문에 타입스크립트에서 문제가 발생한다.
document.monkey = 'Tamarin'; // ~~~~~~ Property 'monkey' does not exist on type 'Document'
TypeScript
복사
더 구체적인 타입 단언문을 사용한다.
interface MonkeyDocument extends Document { /** Genus or species of monkey patch */ monkey: string; } (document as MonkeyDocument).monkey = 'Macaque';
TypeScript
복사
요약
내장 타입에 데이터를 저장해야 하는 경우, 사용자 정의 인터페이스로 단언을 사용하는 것이 좋다.
만약 인터페이스의 보강을 사용하는 경우, 모듈 영역 문제를 인지하고 있어야 한다.

item 44. 타입 커버리지를 추적하여 타입 안전성 유지하기

noImplicitAny를 설정하고 암시적 any 대신 명시적 타입 구문을 추가해도 다음과 같은 any 타입과 관련된 문제들 때문에 안전하다고 할 수 없다.
명시적 any 타입이 존재하는 경우
any 타입의 범위를 좁히고 구체적으로 만들어도 any다.
서드파티에서 any 타입 선언
@types 선언 파일로부터 any 타입이 전파되기 때문에 특별히 조심해야 한다.
any 타입은 타입 안전성과 생산성에 부정적 영향을 미칠 수 있으므로 프로젝트에서 any의 개수를 추적하는 것이 좋다.
npmtype-coverage 패키지를 활용하여 any를 추적할 수 있다.
요약
noImplicitAny가 설정되어 있어도 명시적 any 또는 서드파티 타입 선언을 통해 any 타입이 여전히 코드 내에 존재할 수 있다.
작성한 프로그램의 타입이 적절히 선언되어있는지 추적해야 하며 이를 통해 any의 사용을 줄여나갈 수 있고 타입 안전성을 꾸준히 높일 수 있다.