// 배열 예시
function range(start: number, limit: number) {
const out = []; // 타입이 any[]
for (let i = start; i < limit; i++) {
out.push(i); // out의 타입이 any[]
}
return out; // 타입이 number[]
}
// 단순값 예시
let val; // 타입이 any
if (Math.random()<0.5>) {
val = /hello/;
val; // 타입이 RegExp
} else {
val = 12;
val; // 타입이 number
}
val; // 타입이 number | RegExp
const result = []; // 타입이 any[]
result.push("a");
result; // 타입이 string[]
result.push(1);
result; // 타입이 (string | number)[]
function range(start: number, limit: number) {
const out = [];
// ~~~ 'out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
if (start === limit) {
return out;
// ~~~ 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}
for (let i = start; i < limit; i++) {
out.push(i);
}
return out;
}
unknown은 any 대신 사용할 수 있는 안전한 타입이다.
// Bad
interface Book {
name: string;
author: string;
}
const book: Book = parseYAML(`
name: Wuthering Heights
author: Emily Bronte
`);
alert(book.title); // 오류 없음, 런타임에 "undefined" 경고
book("read"); // 오류 없음, 런타임에 "TypeError: book은 함수가 아닙니다" 예외 발생
// Good
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml);
}
const book = safeParseYAML(`
name: The Tenat of Wildfell Hall
author: Anne Bronte
`);
alert(book.title);
// ~~~ 개체가 'unknown' 형식입니다.
book("read");
// ~~~ 개체가 'unknown' 형식입니다.
any가 위험한 이유
unknown 타입은 any의 첫 번째 속성을 만족하지만, 두 번째 속성은 만족하지 않습니다.
반면 never 타입은 unknown 타입과 정반대입니다.
// 어떠한 값이 있지만 그 타입을 모르는 경우
interface Feature {
id?: string | number;
geometry: Geometry;
properties: unknown;
}
// instanceof를 체크한 후 unknown에서 원하는 타입으로 변환
function processValue(val: unknown) {
if (val instanceof Date) {
val; // 타입이 Date
}
}
declare const foo: Foo;
// Bad
let barAny = foo as any as Bar;
// Good
let barUnk = foo as unknown as Bar;
몽키패치(Monkey Patch)는 런타임 동안에 동적으로 객체에 메서드나 프로퍼티를 추가하는 패턴을 일컫는다. 원래는 예고없이 깜짝스럽게 진행된다는 뜻의 게릴라 패치(Guerrilla Patch)가 비슷한 발음의 고릴라 패치(Gorilla Patch)라고 불리던 중, 비개발자인 CEO에게 좀 위협적으로 들리기 때문에 고릴라 보다는 귀여운 원숭이로 바꿔 부르게 되었다는 유래가 있다.
몽키패치는 안티패턴으로 지양해야하지만 어쩔 수 없이 사용해야하는 경우가 발생할 수 있다. 그런데 타입스크립트에서 몽키패치는 타입체커가 임의로 추가한 속성에 대한 정보를 갖고있지 않다는 점에서 문제가 생긴다.
document.monkey = "Magic";
// Document 타입에 'monkey' 프로퍼티가 없습니다.
이는 interface의 보강(augmentation) 기능을 사용해서 해결할 수 있다.
interface Document {
monkey: string; // 기존의 Document 인터페이스에 monkey 속성이 추가됨.
}
보강과 같이 전역적으로 적용되는 것을 피하고 싶다면 extends 키워드로 확장해서 사용하는 방법이 있다.
interface MonkeyDocument extends Document {
monkey: string;
}
(document as MonkeyDocument).monkey = "Magic";
그러나 몽키 패치를 남용해서는 안 되며, 더 궁극적으로 설계가 잘된 구조로 리팩터링하는 것이 좋다.
noImplicitAny를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입이 여전히 프로그램 내에 존재할 수 있다.
any[]와 { [key: string]: any } 같은 타입은 인덱스를 생성하면 단순 any사 되고 코드 전반에 영향을 미친다.any 타입은 타입 안정성과 생산성에 부정적 영향을 미칠 수 있으므로 프로젝트에서 any의 개수를 추적하는 것이 좋다.
npm의 3가지 종류의 의존성
ts와 관련 라이브러리들은 일반적으로 devDependencies에 속한다.
TS 프로젝트에서 공통적으로 고려해야 할 의존성 두 가지
타입스크립트 자체 의존성 고려하기
타입 의존성(@types) 고려하기
$ npm install react
$ npm install --save-dev @types/react
타입스크립트에서 의존성 관리는 더 복잡하다.
타입스크립트를 사용하면 다음 세 가지 사항을 추가로 고려해야 하기 때문이다.
세 가지 버전 중 하나라도 맞지 않으면 오류가 발생할 수 있다. 이렇게 발생한 오류의 원인을 파악하고 고치기 위해서는 타입스크립트 라이브러리 관리의 복잡한 메커니즘을 모두 이해해야 한다.
타입스크립트에서 일반적으로 의존성을 사용하는 방식은 다음과 같다.
$ npm install react
+ react@16.8.6
$ npm install --save-dev @types/react
+ @types/react@16.8.19
실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식은 다음 네 가지 문제점이 있다.
라이브러리를 업데이트했지만 실수로 타입 선언은 업데이트 하지 않는 경우
라이브러리보다 타입 선언의 버전이 최신인 경우
프로젝트에서 사용하는 타입스크립트 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우
@types 의존성이 중복될 수 있다
node+modules/
@types/
foo/
index.d.ts @1.2.3
bar/
index.d.ts
node_modules/
@types/
foo/
index.d.ts @2.3.4
만약 @types/foo와 @types/bar에 의존하는 경우를 가정해보자. 만약 @types/bar가 현재 프로젝트의 호환되지 않는 버전의 @types/foo에 의존한다면 npm은 중첩된 폴더에 별도로 해당 버전을 설치하여 문제를 해결하려고 한다.
런타임에 사용되는 모듈이라면 괜찮을 수 있지만, 전역 네임스페이스(name-space)에 있는 타입 선언 모듈이라면 대부분 문제가 발생한다.
서드파티의 모듈에서 익스포트되지 않은 타입 정보가 필요한 경우
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export function getGift(name: SecretName, gift: string): SecretSanta {
// ...
}
해당 라이브러리 사용자는 SecretName 또는 SecretSanta를 직접 임포트할 수 없고, getGift만 임포트 가능하다.
이때 익스포트 되지 않은 타입을 추출하는 한 가지 방법은 Parameters와 ReturnType 제너릭 타입을 사용한다.
type MySanta = ReturnType<typeof getGift>; // SecretSanta
type MyName = ReturnType<typeof getGift>[0]; // SecretName
| 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자. 어차피 라이브러리 사용자가 추출할 수 있으므로, 익스포트하기 쉽게 만드는 것이 좋다.
사용자를 위한 문서라면 JSDoc 스타일의 주석으로 만드는 것이 좋다.
// inline 주석
/** JSDoc 주석 */
공개 API에 주석을 붙인다면 JSDoc 형태로 작성해야 한다.
@param과 @returns 같은 일반적 규칙을 사용할 수 있다.@param과 @returns를 추가하면 함수를 호출하는 부분에서 각 매개변수와 관련된 설명을 보여준다./**
* 인사말을 생성합니다.
* @param name 인사할 사람의 이름
* @param title 그 사람의 칭호
* @returns 사람이 보기 좋은 형태의 인사말
*/
function greetFullTSDoc(name: string, title: string) {
return `Hello ${title} ${name};
}
타입 정의에 TSDoc를 사용할 수 있다.
Measurement 객체의 각 필드에 마우스를 올려 보면 필드별로 설명을 볼 수 있다.@param {string} name ...)이 있지만, 타입스크립트에서는 타입 정보가 코드에 있기 때문에 TSDoc에서는 타입 정보를 명시하면 안된다./** 특정 시간과 장소에서 수행된 측정 */
interface Measurement {
/** 어디에서 측정되었나? */
position: Vector3D;
/** 언제 측정되었나? epoch에서부터 초 단위로 */
time: number;
/** 측정된 운동량 */
momentum: Vector3D;
}
| 주석은 간단히 요점만 언급해야 한다.