•
전통적으로 프로그래밍 언어들은 타입 시스템이 정적이거나 동적이거나 둘로 확실히 구분되어 있었다.
•
타입스크립트의 타입 시스템은 선택적이기 때문에 정적이면서도 동적인 특성을 모두 가진다.
•
덕분에 프로그램의 일부분에만 타입 시스템을 적용하며 마이그레이션이 가능한데, 이때 코드의 일부에 타입 체크를 비활성화하는 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의 경우 x는 f1이 종료될 때까지 any인 반면 f2에서는 processBar 호출 이후 x는 여전히 Foo 타입이다.
▪
만약 f1이 return x를 해버린다면 문제는 전파되어 더 복잡해질 것이다.
•
비슷한 관점에서 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다.
◦
함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있기 때문이다.
•
요약
◦
의도치 않은 타입 안전성의 손실을 피하기 위해서 any의 적용 범위를 최소한으로 좁혀야 한다.
◦
함수의 반환 타입이 any인 경우 타입 안전성이 나빠진다. 따라서 any 타입을 반환하는 것은 절대 피해야 한다.
◦
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는 것이 좋다.
item 39. any를 구체적으로 변형해서 사용하기
•
any 타입에는 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스는 물론 null과 undefined도 포함된다.
◦
일반적인 상황에서 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의 경우 out이 number[]로 추론된다.
◦
이는 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를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계다.
•
요약
◦
일반적인 타입들은 정제되기만 하지만 암시적 any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 이유를 인지하고 알고 있어야 한다.
◦
any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 타입 안전성을 확보하는 방법이다.
item 42. 모르는 타입의 값에는 any 대신 unknown 사용하기
•
unknown은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다.
•
unknown은 any의 첫 번째 속성(어떠한 타입이든 할당 가능)을 만족하지만 두 번째 속성(unknown은 오직 unknown과 any에만 할당 가능)은 만족하지 않는다.
◦
반면 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보다 범위가 약간 좁은 타입이다.
◦
{} 타입은 null과 undefined를 제외한 모든 값을 포함한다.
◦
object 타입은 일반적으로 모든 비기본형 타입으로 이루어진다.
▪
이때 기본형임에도 객체와 배열은 포함된다.
•
요약
◦
unknown은 any 대신 사용할 수 있는 안전한 타입으로, 어떠한 값이 있지만 타입을 확신하지 못하는 경우 사용하라.
◦
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 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의 개수를 추적하는 것이 좋다.
◦
npm의 type-coverage 패키지를 활용하여 any를 추적할 수 있다.
•
요약
◦
noImplicitAny가 설정되어 있어도 명시적 any 또는 서드파티 타입 선언을 통해 any 타입이 여전히 코드 내에 존재할 수 있다.
◦
작성한 프로그램의 타입이 적절히 선언되어있는지 추적해야 하며 이를 통해 any의 사용을 줄여나갈 수 있고 타입 안전성을 꾸준히 높일 수 있다.