이펙티브 타입스크립트 정리 - 3장
let x: number = 12; // 이렇게 써도 되긴하지만
let x = 12; // 이렇게 써도 x는 number로 추론된다
const person: {
name: string;
born: {
where: string;
when: string;
};
died: {
where: string;
when: string;
};
} = {
name: "name",
born: {
where: "bornwher",
when: "bornwhen",
},
died: {
where: "diedwhere",
when: "diedwhen",
},
};
const person = {
name: "name",
born: {
where: "bornwher",
when: "bornwhen",
},
died: {
where: "diedwhere",
when: "diedwhen",
},
}; // 객체 또한 동일하게 타입을 지정하지 않아도 추론된다
타입 구문을 제대로 명시할떄와 제거할때
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const { id, name, price } = product;
console.log(id, name, price);
}
// 타입 구문을 제거하였을때
const furby = {
name: "a",
id: 1234,
price: 100,
};
logProduct(furby); // 에러발생, 객체를 선언한곳이아니라 사용되는곳에서 오류가 발생!
// Argument of type '{ name: string; id: number; price: number; }' is not assignable to parameter of type 'Product'.
// Types of property 'id' are incompatible.
// Type 'number' is not assignable to type 'string'
//타입 구문을 제대로 명시했을때 , 실수가 발생한 부분에 오류 발생
const furby: Product = {
name: "a",
id: 1234, // 에러발생 Type 'number' is not assignable to type 'string'.(2322)
price: 100,
};
logProduct(furby);
함수도 반환타입 명시하기!
function getQuote(ticker: string) {
return fetch(`https://quotes.example.com/?1=${ticker}`).then((response) =>
response.json()
);
}
const cache: { [ticker: string]: number } = {};
function getQuote(ticker: string): Promise<number> {
// 함수 반환 타입을 명시하여야 오류의 표시위치가 정확하게 어디인지 알 수 있다.
if (ticker in cache) {
return cache[ticker]; // 해당부분에러 발생
}
return fetch(`https://quotes.example.com/?1=${ticker}`)
.then((response) => response.json())
.then((quote) => {
cache[ticker] = quote;
return quote;
});
}
반환타입을 명시해야하는 이유
- 함수에 대해 더욱 명확하게 할 수 있다.
- 명명된 타입을 사용하기 위해서 이다.
서로 다른 타입에는 별도의 변수를 사용하자
let id = "12-34-56";
fetchProduct(id);
id = 123456; // 에러가 발생, 위에서 string으로 타입추론되어 number형식을 string형식에 할당할수없음!
fetchProdcutById(id);
이유
- 서로 관련이 없는 두개의 타입을 분리
- 변수명을 더 구체적으로 지을 수 있음
- 타입추론을 향상시키며, 타입구문이 불필요해짐
- 타입이 간결해짐
- let대신에 const를 사용하게되어 타입체커가 추론하기 좋음
타입 넓히기 : 타입체커가 타입을 결정해야하는데, 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해내는 과정
interface Vector3 {
x: number;
y: number;
z: number;
}
function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
return vector[axis];
}
let x = "x";
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); //에러발생, Argument of type 'string' is not assignable to parameter of type '"x" | "y" | "z"'
위에서 선언한 let x 의 타입이 string 으로 추론되어서 에러발생
const mixed = ["x", 1]; // 이 코드 하나도 타입스크립트는 다양한 타입으로 추론이 된다
타입스크립트 넓히기 과정을 제어할 수 있는 방법
- const
const x = "x"; // 타입이 'x'이다. let과 다르게 const는 타입 체커가 통과됨
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x);
하지만, const는 만능이 아니다. 앞의 const mixed 예제에서만 봐도 배열에 대한 문제가있음.
객체에서의 타입스크립트 넓히기 알고리즘은 각 요소를 let으로 할당한 것과 같다.
const v = {
x: 1,
}; // x:number 타입으로 추론
v.x = 3; // 성공
v.x = "3"; // 에러
v.y = 4; // 에러
v.name = "name"; // 에러
타입 추론의 강도를 직접 제어하기 위해서는 기본 동작을 재정의 해야함!
기본동작 재정의 하는 3가지 방법
- 명시적 타입 구문 제공
const v: { x: 1 | 3 | 5 } = {
x: 1,
};
- 타입 체커에 추가적인 문맥 제공 (함수 매개변수로 값을 전달)
- 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 a1 = [1, 2, 3]; // number[]
const a2 = [1, 2, 3] as const; // readonly [1,2,3]
타입 좁히기의 가장 대표적인 예시는 null 체크
ex)if문으로 null체크하는 방법
타입 좁히기의 방법은
예외를 던지는 방법, instanceof를 사용, 속성 체크, 내장 함수 등으로 좁힐 수 있다.
예외를 던지는 방법
const el = document.getElementById("foo");
if (!el) throw new Error("Error!!!");
el;
el.innerHTML = "hello";
instancof사용
function contains(text: string, search: string | RegExp) {
if (search instanceof RegExp) {
search;
return !!search.exec(text);
}
search;
return text.includes(search);
}
속성 체크
interface A {
a: number;
}
interface B {
b: number;
}
function pickAB(ab: A | B) {
if ("a" in ab) {
ab; // 타입이 A
} else {
ab; // 타입이 B
}
ab; // 타입이 A | B
}
내장 함수
function contains(text: string, terms: string | string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
termList; // string[]
}
자바스크립트에서 typeof null은 object이므로
const el = document.getElementById("foo");
if (typeof el === "object") {
el;
} // 타입이 따로 좁혀지지 않는다.
명시적으로 태그를 붙이는 방법
interface UploadEvent {type: 'upload'; filname:string; contents: string}
interface DownloadEvent {type: 'download'; filname:string; }
type AppEvent = UploadEvent | DownloadEvent
function handleEvent(e: AppEvent) {
switch(e.type) {
case 'download':
e // type이 DownloadEvent
break;
case 'upload'
e // type이 UploadEvent
break;
}
}
사용자 정의 타입가드
const group = ["a", "b", "c", "d", "e"];
const members = ["f", "e"].map((who) => group.find((n) => n === who)); // type이 const members: (string | undefined)[]
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const members = ["f", "e"]
.map((who) => group.find((n) => n === who))
.filter(isDefined); // type이 const members: string[]
타입추론에 유리하기 위해서는 객체를 생성시에 한꺼번에 생성해야 유리하다.
const pt = {};
pt.x = 3; // 오류가 발생
// 아래와같이 한꺼번에 만들기!
interface Point {
x: number;
y: number;
}
const pt: Point = {
x: 3,
y: 4,
};
객체 전개 연산자 활용하면 타입 걱정 없이 필드 단위로 객체를 생성 할 수도 있다.
const pt = { x: 3, y: 4 };
const id = { name: "abc" };
const namedPoint = {};
Object.assign(namedPoint, pt, id); // 이렇게 쓰는 것 보다
namedPoint.name; // 에러
const namedPoint = { ...pt, ...id }; // 전개연산자를 활용해야한다.
namedPoint.name; // 정상
객체나 배열을 변환해서 새로운 객체나 배열 생성시엔 루프 대신에 내장함수나 Lodash 활용하기!
interface Coordinate {
x: number;
y: number;
}
interface BoundingBox {
x: [number, number];
y: [number, number];
}
interface Polygon {
exterior: Coordinate[];
holes: Coordinate[][];
bbox?: BoundingBox;
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
if (polygon.bbox) {
if (
pt.x < polygon.bbox.x[0] ||
pt.x > polygon.bbox.x[1] ||
pt.y < polygon.bbox.y[0] ||
pt.y > polygon.bbox.y[1]
) {
return false;
}
}
// ...
}
// 코드의 중복을 줄이기 위해 위에는 정상동작하지만, 아래와 같이 변경하면
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const box = polygon.bbox;
if (polygon.bbox) {
if (
pt.x < box.x[0] ||
pt.x > box.x[1] ||
pt.y < box.y[0] ||
pt.y > box.y[1]
) {
// 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
return false;
}
}
// ...
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const box = polygon.bbox;
if (box) {
// box로 바꾸는 것 만으로 에러가 해결
if (
pt.x < box.x[0] ||
pt.x > box.x[1] ||
pt.y < box.y[0] ||
pt.y > box.y[1]
) {
// 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
return false;
}
}
// ...
}
// 객체 비구조화를 활용하여 간결한 문법으로 만들 수 있다.
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const { bbox } = polygon;
if (bbox) {
const { x, y } = bbox;
if (pt.x < x[0] || pt.x > x[1] || pt.y < y[0] || pt.y > y[1]) {
// 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
return false;
}
}
// ...
}
function timeout(millis: number): Promise<never> {
return new Promise((resolve, reject) => {
setTimeout(() => reject("timeout"), millis);
});
}
async function fetchWithTimeout(url: string, ms: number) {
return Promise.race([fetch(url), timeout(ms)]);
}
// 타입 구문이 없어도 반환 타입은 Promise<Response>이다.
Promise도 좋지만, async await를 사용하기
- 코드가 간결하고 직관적
- async함수는 항상 프로미스를 반환하도록 강제함
function getNumber(): Promise<number>;
async function getNumber() {
return 42;
}
const getNumber = async () => 42; // 간단한 형태로 만들수있음
type Language = "Javascript" | "TypeScript" | "Python";
function setLanguage(language: Language) {}
setLanguage("Javascript"); // 정상
let language = "Javascript";
setLanguage(language); // 에러 , Argument of type 'string' is not assignable to parameter of type 'Language'
// 변수로 분리해내면 , 타입스크립트는 할당 시점에 타입을 추론한다.
let language: Language = "Javascript"; // 이렇게 타입 선언에서 가능한 값을 제한하거나
setLanguage(language);
const language = "Javascript"; // let 대신에 const 를 사용하여 타입 체커에게 language 는 변경불가라고 알려줌
setLanguage(language);
튜플 사용 시 주의점
function panTo(where: [number, number]) {}
panTo([10, 20]);
const loc = [10, 20];
panTo(loc); // 에러 발생 , Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
//아래와 같이 수정 가능
const loc: [number, number] = [10, 20]; // 타입 선언
panTo(loc); // 정상
// 타입 단언을 사용할 수 있긴하지만,
const loc = [10, 20] as const;
panTo(loc); // 에러 발생, Argument of type 'readonly [10, 20]' is not assignable to parameter of type '[number, number]'.
// 함수를 수정하는게 낫다.
function panTo(where: readonly [number, number]) {}
// 하지만, 타입 정의에 실수가 있었다고 한다면, 오류가 발생할 수 있다.
객체 사용시 주의점
type Language = "Javascript" | "TypeScript" | "Python";
interface GovernedLanguage {
lanuage: Language;
organize: string;
}
function complain(language: GovernedLanguage) {}
complain({ lanuage: "TypeScript", organize: "microsoft" });
const ts = {
language: "Typescript",
organize: "microsoft",
};
// 타입 추론
// const ts: {
// language: string;
// organize: string;
// }
complain(ts); // 에러 발생, Argument of type '{ language: string; organize: string; }' is not assignable to parameter of type 'GovernedLanguage'.
const ts: GovernedLanguage = {
// 타입선언 추가해줘야함
language: "Typescript",
organize: "microsoft",
};
콜백 사용시 주의점
function callWithRandomNunbers(fn: (n1: number, n2: number) => void) {
fn(Math.random(), Math.random());
}
callWithRandomNunbers((a, b) => {
a; // 타입 number
b; // 타입 number
console.log(a + b);
});
const fn = (a, b) => {
// 에러 발생 Parameter 'a' implicitly has an 'any' type., Parameter 'b' implicitly has an 'any' type.
console.log(a + b);
};
callWithRandomNunbers(fn);
const fn = (a: number, b: number) => {
// 매개변수에 타입 구문을 추가해 해결 가능
console.log(a + b);
};
callWithRandomNunbers(fn);
자바스크립트에 비해서 타입스크립트로 코드를 작성시에 서드파티 라비브러리를 사용하는것이 무조건 유리하다.
타입 정보를 참고하며 작업할 수 있기때문에! 코드도 간결해지고, 시간도 단축!
interface BasketballPlayer {
name: string;
team: string;
salary: number;
}
declare const rosters: { [team: string]: BasketballPlayer[] };
// 반복문을 사용한 단순 목록 생성시
let allPlayers = []; // 에러 발생, ariable 'allPlayers' implicitly has type 'any[]' in some locations where its type cannot be determined.
for (const players of Object.values(rosters)) {
allPlayers = allPlayers.concat(players); // 에러 발생, Variable 'allPlayers' implicitly has an 'any[]' type.
}
let allPlayers: BasketballPlayer[] = []; // 타입선언으로 오류해결
for (const players of Object.values(rosters)) {
allPlayers = allPlayers.concat(players);
}
// 하지만 더 나은 해법으로
const allPlayers = Object.values(rosters).flat(); //오류가난다. flat을 지원하지않는것 같음.
lodash같은 라비르러리나, 내장된 함수형 기법을 통하여 타입스크립트를 안전하게 사용할 수 있다.