
let x: number = 12; // 타입추론 가능
let x = 12; // OK
때로는 우리가 예상한 것보다 더 정확할 때도 있다.
const axis1: string = 'x'; // 타입은 string
const axis2 = 'y'; // 타입은 'y'
비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 한다. 여기서 추가적으로 명시적 타입 구문을 넣는다면 불필요한 타입 선언으로 인해 코드가 번잡해진다.
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const {id, name, price} = product; // Good
const {id, name, price}: {id: string; name: string; price: number} = product; // 이렇게 할 필요가 없다.
}
정보가 부족해 타입스크립트가 스스로 타입을 판단하기 어려운 상황도 있기 때문에 그럴 때는
명시적 타입 구문이 필요하다. 타입스크립는 최종 사용처까지 고려하여 타입 추론을 하지 않기 때문에 변수의 타입은 일반적으로 처음 등장할 때 결정된다.
타입이 추론될 수 있음에도 여전히 타입을 명시하고 싶은 상황도 있다. 예를 들면 객체 리터럴을 정의할 때이다. 타입을 명시하면 잉여 속성 체크가 동작하며, 변수가 사용되는 순간이 아닌 할당하는 시점에 오류가 표시 되도록 해준다.
또한 함수의 반환에도 타입을 명시하여 오류를 방지할 수 있다. 타입 추론이 가능할 지라도 구현상의 오류가 함수를 호출한 곳까지 영향을 미치지 않도록 하기 위해 타입 구문을 명시하는 것이 좋다.
추론할 수 있다면 타입 구문을 작성하지 않는 게 좋다.함수/메서드 시그니처에 타입 구문을 포함하고 함수 내에서 생성된 지역변수에는 타입 구문을 넣지 않는다.let id = "12-34-56";
sum(id);
id = 123456;
fetchProduct(id); // Error
// string 형식의 인수는 number형식의 매개변수에 할당될 수 없습니다.
여기서 변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다! 는 중요한 관점을 알 수 있다.
변수를 무분별하게 재사용하면 타입 체커와 사람 모두에게 혼란을 줄 뿐이다.
다른 타입에는 별도의 변수를 사용하는 게 바람직한 이유는 다음과 같다.
가능한 값들의 집합인 타입을 갖는다.넓히기(widening)라고 부른다.interface Vector3 { x: number, y: number; z: number }
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis];
}
let x = 'x'; // string 타입으로 추론
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x); // ~ 'string' 형식의 인수는 '"x" | "y" | "z"' 형식의 매개변수에 할당될 수 없다.
const x = 'x'; // 'x' 타입으로 추론
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x); // ok
x는 재할당될 수 없으므러 타입스크립트는 더 좁은 타입인 'x' 타입으로 추론할 수 있다. 하지만 객체나 배열같은 참조형 데이터에는 문제가 발생한다.
객체의 경우 타입스크립트의 넓히기 알고리즘은 각 요소를
let으로 할당된 것처럼 다룬다.
타입 추론의 강도를 직접 제어하려면 타입스크립트의 기본 동작을 재정의해야 한다.
const v: {x: 1|3|5} = {
x: 1
} // 타입은 {x: 1|3|5}
const 단언문은 온전히 타입 공간의 기법이다.as const를 작성하면 최대한 좁은 타입으로 추론한다.const v1 = {
x: 1,
y: 2,
}; // {x: number, y: number}
const v2 = {
x: 1 as const,
y: 2,
}; // {x: 1, y: number}
const v3 = {
x: 1,
y: 2,
} as const; //{readonly x: 1, readonly y: 2}
const, 타입 구문, 문맥, as const에 익숙해지자.타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다. null 체크를 하거나 instanceof, 속성 체크 등으로 타입을 좁힐 수 있다. 또는 Array.isArray() 같은 일부 내장 함수로도 타입을 좁힐 수 있다.
타입스크립트는 일반적으로 조건문에서 타입을 좁히는 데 매우 능숙하다. 만약 타입스크립트가 타입을 식별하지 못한다면, 식별을 돕기 위해 커스텀 함수를 도입할 수 있다.
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement) {
if(isInputElement(el)) {
el; // 타입이 HTMLInputElement
return el.value;
}
el;
return el.textContent;
}
이러한 기법을 사용자 정의 타입 가드라고 한다. 반환 타입의 el is HTMLInputElement는 함수의 반환이 true인 경우, 타입 체커에게 타입을 좁힐 수 있다고 알려 준다.
어떤 함수들은 타입 가드를 사용하여 배열과 객체의 타입 좁히기를 할 수 있다.
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael'];
const members = ['Janet', 'Micheal'].map(
who => jackson5.find(n => n === who)
).filter(who => who !== undefined); // 타입이 (string | undefined)[]
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const members2 = ['Janet', 'Micheal'].map(
who => jackson5.find(n => n === who)
).filter(isDefined); // 타입이 string[]
const pt = { x: 3, y: 4 };
const id = { name: "Pythagoras" };
const namePoint = { ...pt, ...id };
namePoint.name;
다음과 같이 객체 전개 연산자 '...'을 사용해 큰 객체를 한꺼번에 만들어 낼 수 있다.
타입에 안전한 방식으로 조건부 속성을 추가하려면, 속성을 추가하지 않는 null 또는 {} 으로 객체 전개를 사용하면 된다.
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? { middle: '5' } : {})};
편집기에서 president를 보면 타입이 선택적 속성을 가진 것으로 추론된다는 것을 알 수 있다.
const president: {
middle?: string;
first: string;
last: string;
}
전개 연산자로 한꺼번에 여러 속성을 추가할 수도 있다.
declare let hasDates: boolean;
const nameTitle = { name: 'Khufu', title: 'Pharaoh' };
const pharaoh = {
...nameTitle,
...(hasDates ? {start: -2589, end: -2566} : {})
};
// 타입 (책과 다르다? page130)
const pharaoh: {
start?: number | undefined;
end?: number | undefined;
name: string;
title: string;
}
만약 이를 선택적 필드 방식으로 표현하려면 다음처럼 헬퍼 함수를 사용하면 된다.
function addOptional<T extends object, U extends object>(
a: T, b: U | null
): T & Partial<U> {
return { ...a, ...b };
}
const pharaoh = addOptional(
nameTitle,
hasDates ? { start: -2589, end: -2566 } : null
);
pharaoh.start // 정상, 타입이 number | undefined
가끔 객체나 배열을 변환해서 새로운 객체나 배열을 생성하고 싶을 수 있다. 이런 경우 루프 대신 내장된 함수형 기법 또는 로대시(Lodash)같은 유틸리티 라이브러리를 사용하는 것이 '한꺼번에 객체 생성하기' 관점에서 보면 옳다.
객체 전개를 사용하자.const borough = {name: 'Brooklyn', location: [40.688, -73.979]};
const loc = borough.location; // borough.location에 loc 별칭 사용
loc[0] = 0;
borough.location; // [0, -73.979] 원래 속성값도 0으로 변경되서 나타남
별칭을 남발해서 사용하면 제어 흐름을 분석하기 어렵다. 별칭을 신중하게 사용해야 코드를 잘 이해할 수 있고, 오류도 쉽게 찾을 수 있다.
지역변수를 사용하면 타입 정제를 믿을 수 있다.ES5 또는 더 이전 버전을 대상으로 할 때, 타입스크립트 컴파일러는 async와 await가 동작하도록 정교한 변환을 수행한다. 다시 말해, 타입스크립트는 런타임에 관계없이 async/await를 사용할 수 있다.
콜백보다 프로미스나 async/await를 사용해야 하는 이유는 다음과 같다.
async function fetchPage() {
const [response1, response2, response3] = await Promise.all([
fetch(url1), fetch(url2), fetch(url3)
]);
// ...
}
선택의 여지가 있다면 프로미스를 생성하기보다는 async/await를 사용해야 한다.
async와 await를 사용하는 것이 좋다. 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수 있다.async로 선언하자!타입스크립트는 타입을 추론할 때 단순히 값만 고려하지는 않는다. 값이 존재하는 곳의 문맥까지 살핀다. 문맥을 고려하다 보면 가끔 이상한 결과가 나온다.
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }
// 인라인 형태
setLanguage('JavaScript'); // 정상
// 참조 형태
let language = 'JavaScript';
setLanguage(language); // 'string' 타입의 인수는 'language' 형식의 매개변수에 할당할 수 없습니다.
값을 변수로 분리하면 타입스크립트는 할당 시점에 타입을 추론한다. 위의 경우는 string으로 추론했고, Language 타입으로 할당이 불가능하므로 오류가 발생하였다.
이러한 문제를 해결하는 데에는 두 가지 방법이 있다.
let language: Language = 'JavaScript';
setLanguage(language); // 정상
const language = 'JavaScript';
setLanguage(language); // 정상
function panTo(where: [number, number]) { /* ... */ }
panTo([10, 20]); // 정상
const loc = [10, 20]; // number[] 로 추론
panTo(loc); // 'number[]' 형식의 인수는 '[number, number]' 형식의 매개변수에 할당할 수 없다.
// 해결방안 타입 선언 제공
const loc: [number, number] = [10,20];
panTo(loc); // ok
// 함께 readonly로 만들기
function panTo(where: readonly [number, number]) { /* ... */ }
const loc = [10,20] as const;
panTo(loc); // ok
as const는 문맥 손실과 관련된 문제를 해결할 수 있지만, 타입 정의에 실수가 있다면 오류는 타입 정의가 아니라 호출되는 곳에서 발생한다는 단점이 있다. 그렇기에 복잡한 객체라면 오류가 발생한 근본적인 원인을 파악하기 어려울 수도 있다.
타입 선언을 추가하거나 상수 단언(as const)을 사용해 해결한다.콜백을 다른 함수로 전달할 때, 타입스크립트는 콜백의 매개변수 타입을 추론하기 위해 문맥을 사용한다. 콜백은 상수로 뽑아내면 문맥이 소실되고 noImplicitAny 오류가 발생하게 되는데, 이런 경우는 매개변수에 타입 구문을 추가해서 해결할 수 있다.
가능하면 전체 함수 표현식에 타입 선언을 적용하자!
상수 단언을 사용해야 한다. 상수 단언을 사용하면 오류가 정의한 곳이 아니라 사용한 곳에서 발생하므로 주의하자. jQuery, UnderScore, Lodash, Ramda 같은 라이브러리들의 일부 기능(map, flatMap, filter, reduce)은 순수 자바스크립트로 구현되어 있다. 이들은 타입스크립트와 조합하면 더 빛을 발한다. 그 이유는 타입 정보가 그대로 유지되면서 타입 흐름(flow)이 계속 전달되도록 하기 때문이다.
타입스크립트는 서드파티 라이브러리를 사용하는 것이 무조건 유리하다. 타입 정보를 참고하며 작업할 수 있기 때문에 서브파티 라이브러리 기반으로 바꾸는 데 시간이 훨씬 단축되기 때문이다.