이펙티브 타입스크립트(5)
5-1. any 타입은 가능한 한 좁은 범위에서만 사용하기
•
TypeScript 의 타입 시스템은 선택적(Optional) 이고 점진적(Gradual) 이기 때문에 정적이면서도 동적인 특성을 동시에 가진다
•
any 는 좁은 범위에서 사용해서 한다
◦
예시 코드
▪
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
}
TypeScript
▪
function f1() {
const x = expressionReturningFoo();
processBar(x as any);
}
TypeScript
•
타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다
◦
함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있다
•
객체를 선언할 때도 최소한의 범위에서만 any 를 사용해야 한다
◦
예시 코드
▪
const config: Config = {
a: 1,
b: 2,
c: {
key: value,
}
} as any;
TypeScript
▪
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any,
}
}
TypeScript
요약
•
의도치 않은 타입 안정성의 손실을 피하기 위해서 any 의 사용 범위를 최소한으로 좁혀야 한다
•
함수의 반환 타입이 any인 경우 타입 안정성이 나빠진다
•
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore 를 사용해야 한다
5-2. any 를 구체적으로 변형해서 사용하기
•
일반적인 상황에서 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높기 때문에, 구체적인 타입을 찾아 타입 안정성을 높여야 한다
◦
예시 코드
▪
function getLengthBad(array: any) {
return array.length;
}
TypeScript
▪
•
함수 내의 array.length 타입 체크
•
함수 반환 타입이 any 대신 number 로 추론
•
함수 호출될 때 매개변수가 배열인지 체크
function getLength(array: any[]) {
return array.length;
}
TypeScript
•
매개변수가 객체이지만 값을 알 수 없다면 모든 비기본형(non-primitive) 타입을 포함하는 object 타입을 사용할 수 있다
◦
예시 코드
function hasTwelveLetterKey(o: {[key: string]: any}) {
// ...
}
function hasTwelveLetterKey(o: Object) {
// ...
}
TypeScript
요약
•
any 를 사용할 때는 정말로 모든 값이 허용되어야하는지 확인해야 한다
•
any 보다 더 정확하게 모델링할 수 있도록 구체적인 형태를 사용해야 한다
5-3. 함수 안으로 타입 단언문 감추기
•
함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 고려해가며 타입 정보를 힘들게 구성할 필요는 없다
◦
함수 내부에는 타입 단언을 사용하고, 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는것이 좋다
요약
•
타입 선언문은 일반적으로 타입을 위험하게 만들지, 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다
•
불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨겨야 한다
5-4. any 의 진화를 이해하기
•
타입스크립트에서는 일반적으로 변수의 타입은 변수를 선언할 때 결정된다
◦
그 후에 정제될 수 있지만, 새로운 값이 추가되도록 확장할 수는 없다
◦
any 타입과 관련해서 예외인 경우가 존재한다
◦
예시 코드
function range(start: number, limit: number) {
const out = [];
for (let i = start; i < limit; i++) {
out.push(i);
}
return out; // 반환 타입이 number[] 로 추론됨
}
TypeScript
•
any 의 타입 진화(evolve)는 noImplicitAny 가 설정된 상태에서 변수의 타입이 암시적 any 인 경우에만 일어난다
◦
타입의 진화는 값을 할당하거나 배열에 요소를 넣은 후에만 일어나기 때문에, 편집기에서는 이상하게 보일 수 있다
•
타입을 안전하게 지키기 위해서는 암시적 any 를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계이다
요약
•
일반적인 타입들은 정제되기만 하는 반면, 암시적 any 와 any[] 타입은 진화할 수 있다
•
any 를 진화시키는 방식보다 명시적 타입 구문을 사용하는것이 안전한 타입을 유지하는 방법이다
5-5. 모르는 타입의 값에는 any 대신 unknown을 사용하기
•
any 가 강력하면서도 위험한 이유
◦
어떠한 타입이든 any 타입에 할당이 가능하다
◦
any 타입은 어떠한 타입으로도 할당이 가능하다
◦
any 를 사용하면 타입 체커가 무용지물이 된다
•
unknown 타입은 any의 첫번째 속성을 만족하지만, 두번째 속성은 만족하지 않는다
◦
어떠한 타입이든 unknown 에 할당 가능
◦
unknown 은 오직 unknown 과 any 에만 할당 가능
•
unknown 타입인 채로 값을 사용하면 오류가 발생하기 때문에 적절한 타입으로 변환하도록 강제할 수 있다
•
타입 단언문 외에 instanceof, typeof, 사용자 정의 가드 등을 통해서도 원하는 타입으로 변환할 수 있다
◦
예시 코드
function processValue(val: unknown) {
if (val instanceof Date) {
val // 타입이 Date
}
}
TypeScript
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; // 타입이 Book
}
}
TypeScript
•
제네릭을 사용한 스타일은 타입 단언문과 달라 보이지만, 기능적으로는 동일하다
◦
제네릭보다는 unknown 을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다
◦
예시 코드
function safeParseYAML<T>(yaml: string): T {
return parseYAML(yaml);
}
TypeScript
•
{} 타입은 null 과 undefined 를 제외한 모든 값을 포함한다
•
object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다
•
unknown 이 도입되기 전에는 {} 가 더 일반적으로 사용되었지만, 최근에는 {} 를 사용하는 경우가 드물다
요약
•
unknown 은 any 를 대신할 수 있는 안전한 타입이다
◦
어떠한 값이 있지만, 그 타입을 알지 못하는 경우라면 unknown을 사용하면 된다
•
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown 을 사용하면 된다
5-6. 몽키 패치보다는 안전한 타입을 사용하기
•
자바스크립트에서 객체나 클래스에 임의의 속성을 추가할 수 있다
◦
타입스크립트 타입 체커는 글로벌 변수에 내장 속성에 대한 내용은 알지만, 임의로 추가한 속성에 대해서 알지 못하기 때문에 에러가 발생한다
•
임의로 추가한 속성에 대한 에러를 처리하는 방법
1.
타입 단언문 사용
•
타입 안정성을 상실하고, 언어 서비스를 사용할 수 없게됨
(document as any).monkey = 'Tamarin';
TypeScript
2.
interface의 특수 기능 중 하나인 보강 (augmentation) 사용
•
any 보다 더 나은점
◦
타입이 더 안전하다
◦
속성에 주석을 붙일 수 있다
◦
속성에 자동 완성을 사용할 수 있다
◦
몽키 패치가 어떤 부분에 적용되었는지 정확한 기록이 남는다
•
보강을 사용할 때 주의해야 할 점은 모듈 영역(scope) 와 관련이 있다
◦
보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다
interface Document {
/** 몽키 패치의 속(genus)또는 종(species) */
monkey: string;
}
document.moneky = 'Tamarin';
TypeScript
export {};
declare global {
interface Document {
/** 몽키 패치의 속(genus)또는 종(species) */
monkey: string;
}
}
TypeScript
3.
더 구체적인 타입 단언문 사용
•
Document 를 확장하기 때문에 타입 단언문은 정상이며, 할당문의 타입은 안전하다
•
새로운 타입을 도입했기 때문에 모듈 영역 문제도 해결할 수 있다
•
몽키 패치를 남용해서는 안되며 궁극적으로 더 잘 설계된 구조로 리팩터링하는 것이 좋다
interface MonkeyDocument extends Document {
/** 몽키 패치의 속(genus)또는 종(species) */
monkey: string;
}
(document as MonkeyDocument).monkey = 'Macaque';
TypeScript
요약
•
전역 변수나 DOM에 데이터를 저장하지 말고, 데이터를 분리하여 사용해야 한다
•
내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나를 사용해야 한다
5-7. 타입 커버리지를 추적하여 타입 안정성 유지하기
•
noImplicitAny 를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제로부터 안전하다고 할 수 없다
•
any 타입이 프로그램 내에 존재할 수 있는 경우
1.
명시적 any타입
•
any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입이다
•
특히 any[] 와 {[key: string]: any} 같은 타입은 인덱스를 생성하면 단순 any 가 되고, 코드 전반에 영향을 끼친다
2.
서드파티 타입 선언
a.
@types 선언 파일로부터 any 타입이 전파되기 때문에 특별히 조심해야 한다
b.
noImplicitAny 를 설정하고 절대 any 를 사용하지 않았다 하더라도 여전히 any 타입은 코드 전반에 영향을 끼친다
•
프로젝트에서 any 의 개수를 추적하는 방법
◦
type-coverage 패키지를 활용하면 any 를 추적할 수 있다
◦
--detail 플래그를 붙이면, any 타입이 있는 곳을 모두 출력해준다
▪
이를 통해서 예상치 못한 any 의 근원지를 찾을 수 있다
$ npx type-coverage
9985 / 10117 98.69%
Shell
요약
•
noImplicitAny 가 설정되어 있어도, 명시적 any 또는 서드파티 타입 선언을 통해 any 타입은 코드 내에 여전히 존재할 수 있다
•
작성한 프로그램의 타입이 얼마나 잘 선언되었는지 추적해야 한다