아이템 41 any의 진화를 이해하기
요약
- 일반적인 타입들은 정제되기만 하는 반면 (ex. 타입 좁히기), 암시적 any와 any[] 타입은 진화할 수 있습니다.
이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야합니다. - any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법 입니다.
1. 일반적인 타입들은 정제되기만 하는 반면, 암시적 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
2. 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과 undefined를 제외한 모든 값을 포함한다.
1. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 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;
2. unknown는 모든 타입의 상위 타입이고, 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' 형식에 할당할 수 없습니다
3. {} 타입은 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 타입이 있는 곳을 모두 출력할 수 있다.
$ npx type-coverage
// 9985 / 10117 98.69%
6장 타입 선언과 @types
아이템 45 devDependencies 에 typescript와 @types 추가하기
요약
- typescript를 시스템 레벨(npm install -g typescript)로 설치하면 안 된다.
typescript를 프로젝트의 devDependencies 에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 한다. - @types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 한다.
런타임에 @types 가 필요한 경우라면 별도의 작업이 필요할 수 있다.
예시
//package.json
{
...
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.0.2",
"@types/lodash": "^4.14.192",
"@types/jest": "^29.5.0",
"jest": "^29.5.0"
}
}
- dependencies
- 현재 프로젝트를 실행하는 데 필수적인 라이브러리들이 포함
- 프로젝트를 npm에 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, dependencies에 들어 있는 라이브러리도 함께 설치될 것
- devDependencies
- 현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들이 포함됨
모든 타입스크립트 프로젝트에서 공통적으로 고려해야 할 의존성 2가지
1. 타입스크립트 자체 의존성을 고려해야 한다.
- 시스템 레벨로 설치할 수 있지만, 팀원들 모두 동일한 버전을 설치한다고 보장할 수 없고, 프로젝트를 셋업할 때 별도의 단계가 추가됨
- 따라서 타입스크립트를 시스템 레벨로 설치하기보다는 devDependencies에 넣는 것이 좋다.
devDependencies에 포함되어 있다면, npm install을 실행할 때 팀원들 모두 항상 정확한 버전의 타입스크립트를 설치할 수 있습니다.
2. 타입 의존성(@types)을 고려해야 한다.
DefinitelyTyped
타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리의 타입을 정의한 모음
사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도, DefinitelyTyped에서 타입을 얻을 수 있다.
-> 원본 라이브러리 자체가 dependencies에 있더라도 ©types 의존성은 dev Dependencies에 있어야 합니다.
예시. 리액트의 타입 선언과 리액트를 의존성에 추가하는 명령어
$ npm install react
$ npm install --save-dev @types/react
그러면 다음과 같은 package.json 파일이 생성됨
{
"devDependencies": {
"@types/react": "~L6.8.19",
"typescript": "3.5.3"
},
"dependencies": {
"react": "16.8.6"
}
}
아이템 46 타입 선언과 관련된 세 가지 버전 이해하기
요약
- 타입 선언과 관련된 세가지 버전이 있다. 라이브러리 버전, @types 버전, typescript 버전이다. 이 세가지의 버전에 따라 타입스크립트 생태계에선 더욱 의존성이 관리가 복잡하게 되었다.
- 라이브러리를 업데이트하는 경우, 해당 @types 역시 업데이트를 하자.
- 라이브러리를 만들때 타입 선언을 관리하는 방법은 두가지가 있다.
- 방법1) 타입 선언을 DefinitelyTyped에 공개(= @types)해 라이브러리와 따로 두기
- 방법2) 타입 선언을 라이브러리에 포함시키기
- 공식적인 권장사항은 타입스크립트로 작성된 라이브러리인 경우만 타입 선언을 자체적으로 포함하는 것이다. 그 외의 경우(자바스크립트로 작성된 라이브러리)라면 타입을 DefinitelyTyped에 공개해 따로 관리하는게 좋다.
실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점
// 특정 라이브러리 - dependencies로 설치
$ npm install react
+ react@16.8.6
// 타입 정보 - devDependencies
$ npm install —save-dev @types/react
+ @types/react@16.8.19
-> 메이저 버전과 마이너 버전(16.8)이 일치하지만 패치 버전(.6과 .19)은 일치하지 않다.
1. 라이브러리를 업데이트했지만 실수로 타입 선언은 업데이트하지 않는 경우
라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때마다 타입 오류가 발생하게 됨
2. 라이브러리보다 타입 선언의 버전이 최신인 경우
타입 정보 없이 라이브러리를 사용해 오다가 타입 선언을 설치하려고 할 때 발생하게 됨
-> 타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에 실제로 쓰이는 것은 과거 버전
3. 프로젝트에서 사용하는 타입스크립트 버전보다 라이브러 리에서 필요로 하는 타입스크립트 버전이 최신인 경우
-> 프로젝트의 타입스크립트 버전을 올리거나 or
라이브러리 타입 선언의 버전을 원래대로 내리거나 or
declare module 선언으로 라이브러리의 타입 정보를 없애 버리기
4. @types 의존성이 중복될 수 있음
아이템 47 공개 API에 등장하는 모든 타입을 익스포트하기
요약
- 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자.
어차피 라이브러리 사용자가 추출할 수 있으므로 익스포트 하기 쉽게 만드는 것이 좋다. (어차피 숨기는게 거의 불가능하다.)
예시
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export const getGift = (name: SecretName, gift: string): SecretSanta => {
//...
};
// 타입 추출하기
type MySanta = ReturnType<typeof getGift>; //SecretSanta
type MyName = Parameters<typeof getGift>[0]; //SecretName
'Javascript & Typescript' 카테고리의 다른 글
[Typescript] 이펙티브타입스크립트 (#34 ~ #40) (0) | 2024.11.03 |
---|---|
[Typescript] 이펙티브타입스크립트 (#1 ~ #5) (1) | 2024.09.14 |
[javascript] forEach, map, filter, reduce 메서드 작동원리 (1) | 2023.03.02 |