Closed moonheekim0118 closed 1 year ago
해피 땡스기빙 이펙티브 타입스크립트 본격 출발!! 🚗🚗🚗
타입스크립트의 많은 타입 구문은 사실 불필요하다. 타입 추론이 된다면 명시적 타입 구문을 작성하지 않는게 좋다. 타입스크립트는 예상한 것보다 더 정확하게 추론하기도 한다.
const axis1: string = 'x'; // 타입은 string
const axis2 = 'y'; // 타입은 "y"
// axios2 변수를 string으로 예상하기 쉽지만,
// 타입스크립트가 추론한 "y"가 더 정확한 타입이다.
이상적인 경우 함수/메서드의 시그니처에는 타입 구문이 있지만, 함수 내의 지역 변수에는 타입 구문이 없다.
명시적 타입 구문보다 비구조화 할당문을 사용하여, 모든 지역 변수의 타입이 추론되도록 하자. 지역변수에 추가로 명시적 타입을 넣으면 불필요한 타입 선언으로 코드가 번잡해진다.
interface Product {
id: number;
name: string;
price: number;
}
// 아래의 경우,
// Product의 id의 타입이 바뀌면 logProduct 내의 id 변수 선언 타입과 맞지 않아
// 오류가 발생한다.
function logProduct(product: Product) {
const id: number = product.id;
const name: string = product.name;
const price: number = product.price;
}
// 명시적 타입 구문 없이 비구조화 할당문으로 구현하면, 지역 변수의 타입이 추론된다.
// 따라서 Product 내 속성의 타입이 바뀌어도 오류가 발생하지 않는다.
function logProduct(product: Product) {
const {id, name, price} = product;
console.log(id, name, price);
}
// 비구조화 할당을 하면서 명시적 타입을 주면 코드가 번잡해 진다.
function logProduct(product: Product) {
const {id, name, price}: {id: string; name: string; price: number} = product;
console.log(id, name, price);
}
타입스크립트가 타입을 판단하기 어려운 경우에는 명시적 타입 구문이 필요하다.
명시적 타입 구문이 필요한 상황
객체 리터럴
객체 리터럴에 타입 구문을 명시하면, 잉여 속성 체크가 동작하여 실제로 실수가 발생한 부분에 오류를 표시해준다. 타입 추론이 가능할지라도 오류의 영향 범위가 커지지 않도록 하기 위해 타입 구문을 명시하는 게 좋다.
타입 구문을 명시하여 잉여 속성 체크가 동작하면 변수를 할당하는 시점에 오류가 표시된다. 타입 구문이 없는 경우 잉여 속성 체크가 동작하지 않고, 객체를 선언한 곳이 아니라 객체가 사용되는 곳에서 타입 오류가 발생한다.
const furby = {
name: 'Furby',
id: 123456,
price: 35
}
// 아래의 경우, furby를 사용하는 곳에서 에러가 발생하는 경우다.
// logProduct의 매개변수 타입 Product의 id 속성 타입과,
// 객체 리터럴 furby의 id 속성 타입이 일치하지 않아 오류가 발생한다.
logProduct(furby);
// ... 형식의 인수는 'Product' 형식의 매개변수에 할당될 수 없습니다.
// 'id' 속성의 형식이 호환되지 않습니다.
// 'number' 형식은 'string' 형식에 할당할 수 없습니다.
// 실제 오류는 객체를 만드는 과정에서 발생했다.
// 객체 리터럴에 타입 구문을 제대로 명시하면 실수가 발생한 부분에 오류를 표시해준다.
const furby: Product = {
name: 'Furby',
id: 123456,
// 'number' 형식은 'string' 형식에 할당할 수 없습니다.
price: 35
}
의도된 반환 타입
함수의 반환 타입 지정 없이, 함수 내부에서 잘못된 값을 반환하는 경우, 오류는 함수 내부가 아닌 함수를 호출한 코드에서 발생한다. 이때 의도된 반환 타입을 명시하여 오류의 위치를 제대로 표시하도록 한다.
// 주식 시세를 조회하는 함수를 작성해본다.
function getQuote(ticket: string) {
return fetch(`https://quotes.example.com/?q=${ticker}`)
.then(response => response.json());
}
// 캐시를 추가하여 다시 작성해본다.
const cache: {[ticker: string]: number} = {};
function getQuote(ticker: string) {
if (ticker in cache) {
return cache[ticker]; // number 타입
}
// Promise<any> 타입
return fetch(`https://quotes.example.com/?q=${ticker}`)
.then(response => response.json())
.then(quote => {
cache[ticker] = quote;
return quote;
});
}
// 위 코드는 반환 타입이 number | Promise<any>로 작성되는 오류가 있다.
// 의도된 반환 타입 Promise에 대한 명시가 없기 때문에 구현부에 오류가 발생하지 않는다.
// 오류는 함수를 호출하는 코드, 즉 사용자 코드의 오류로 표시된다.
getQuote('MSFT').then(considerBuying);
// 'number | Promise<any>' 형식에 'then' 속성이 없습니다.
// 'number' 형식에 'then' 속성이 없습니다.
// 의도된 반환 타입을 명시하여 오류의 위치를 제대로 표시하도록 한다.
const cache: {[ticker: string]: number} = {};
function getQuote(ticker: string): Promise<number> {
if (ticker in cache) {
return cache[ticker];
// 'number' 형식은 'Promise<number>' 형식에 할당할 수 없습니다.
}
// ...
}
미리 함수 시그니처를 작성하고 타입을 명시하는 방법은, 함수 구현 전에 테스트를 먼저 작성하는 TDD와 비슷하다. 전체 타입 시그니처를 먼저 작성하면 구현에 맞추어 시그니처가 작성되는 것을 방지하고 제대로 원하는 모양을 얻게 된다.
반환 타입으로 명명된 타입을 사용하기 위해서는 반환 타입 명시가 필요하다.
interface Vector2D { x: number; y: number; }
// 다음 함수에서는 반환 타입을 명시하지 않았다.
// 타입스크립트는 반환 타입을 { x: number; y: number; }로 추론한다.
function add(a: Vector2D, b: Vector2D) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
// 이 경우, 입력은 명명된 타입(Vector2D)을 가지지만,
// 반환 타입은 추론된 타입({ x: number; y: number; })이기 때문에
// 사용자 입장에서 당황스러울 수 있다.
린터(linter)를 사용하고 있다면 eslint 규칙 중 no-inferrable-types
을 사용해서 작성된 모든 타입 구문이 정말로 필요한지 확인할 수 있다.
자바스크립트에서는 한 변수를 다른 목적을 가지는 다른 타입으로 재사용해도 된다.
반면 ‘변수의 값은 바뀔 수 있지만 그 타입은 일반적으로 바뀌지 않는다’는 관점에서, 타입스크립트에서는 오류가 발생한다.
타입이 다른 값에 변수를 재사용하는 것은 혼란을 준다. 다른 타입에는 별도의 변수를 사용하는게 바람직하다. 그 이유는 다음과 같다.
타입스크립트는 값을 가지고 변수에 할당 가능한 값들의 집합을 유추해야 한다. 이를 ‘타입 넓히기(widening)’라고 한다. 이때 타입스크립트는 작성자의 의도를 추측하면서 명확성과 유연성 사이의 균형을 유지하려고 노력한다. 오류를 잡기에 충분히 구체적으로 추론해야 하지만, 잘못된 추론을 할 정도로 구체적으로 수행하지는 않는다.
const mixed = ['x', 1];
위 mixed의 타입이 될 수 있는 후보들은 다음과 같다. 실제로는 (string|number)[]
로 추측한다.
타입스크립트의 타입 넓히기 과정을 제어하는 방법
const로 변수 선언하기
let 대신 const로 변수를 선언하면 해당 변수는 재할당될 수 없으므로 타입스크립트는 의심의 여지 없이 더 좁은 타입으로 추론할 수 있다.
타입 추론의 강도 제어를 위한 타입스크립트의 기본 동작 재정의
명시적 타입 구문 제공하기
타입 체커에 추가적인 문맥 제공하기 (예를 들어, 함수의 매개변수로 값을 전달)
⇒ 아이템 26
const 단언문 사용하기
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};
// 배열을 튜플 타입으로 추론할 때도 as const 사용 가능
const a1 = [1, 2, 3]; // 타입은 number[]
const a2 = [1, 2, 3] as const; // 타입은 readonly [1, 2, 3]
타입 좁히기는 타입스크립트가 넒은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다. 타입스크립트는 일반적으로 조건문에서 타입 좁히기를 잘 해낸다.
null 체크
typeof
로 체크하는 경우, typeof null
이 object임을 주의할 것.const el = document.getElementById('foo'); // 타입이 HTMLElement | null
if (el) {
el // 타입이 HTMLElement
} else {
el // 타입이 null
}
분기문에서 예외를 던지거나 함수 반환하여 블록의 나머지 부분에서 변수의 타입 좁히기
const el = document.getElementById('foo'); // 타입이 HTMLElement | null
if (!el) {
throw new Error('Unable to find #foo');
}
el // 타입이 HTMLElement
instanceof
사용하기
function contains(text: string, search: string|RegExp) {
if (search instanceof RegExp) {
search // 타입은 RegExp
return !!search.exec(text);
}
search // 타입은 string
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
}
내장 함수 사용하기 (ex. Array.isArray)
function contains(text: string, terms: string | string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
termList // 타입 string[]
}
명시적 ‘태그’ 붙이기
이 패턴은 태그된 유니온(tagged union) 또는 구별된 유니온(discriminated union)이라고 부른다.
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case 'download' :
e
break;
case 'upload' :
e
break;
}
}
사용자 정의 타입 가드 사용하기
반환 타입을 el is HTMLInputElement
로 지정된 함수의 반환이 true인 경우, 함수는 타입 체커에게 매개변수의 타입을 해당 타입으로 좁힐 수 있다고 알려준다.
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el // 타입 HTMLInputElement
return el.value;
}
el // HTMLElement
return el.textContent;
}
// 타입 가드를 통해배열과 객체의 타입 좁히기를 할 수 있다.
// 배열에서 어떤 탐색 수행 시 undefined가 될 수 있는 타입을 사용하게 되는데,
// 이때 타입 가드를 사용하여 타입을 좁힐 수 있다.
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael'];
const members1 = ['Janet', 'Michael'].map(
who => jackson5.find(n => n === who)
); // 타입 (string|undefined)[]
const members2 = ['Janet', 'Michael'].map(
who => jackson5.find(n => n === who)
).filter(who => who !== undefined); // 타입 (string|undefined)[]
const members3 = ['Janet', 'Michael'].map(
who => jackson5.find(n => n === who)
).filter(isDefined); // 타입 string[]
객체 생성 시, 필요한 속성을 모두 담아 가능한 한꺼번에 객체로 만들어야 한다.
객체 전개 연산자를 사용하면 작은 객체를 모아 큰 객체를 한꺼번에 만들어 낼 수 있고, 타입 걱정 없이 필드 단위로 객체를 생성할 수 있다. 이때 모든 업데이트마다 새 변수를 사용하여 각각 새로운 타입을 얻도록 해야 한다.
interface Point { x: number; y: number; }
const pt0 = {};
const pt1 = { ...pt0, x: 3 };
const pt2: Point = { ...pt1, y: 4 };
타입에 조건부 속성을 추가하려면, 속성을 추가하지 않는 null이나 {}과 함께 객체 전개를 사용한다.
declare let hasMiddle: boolean;
const firstLast = { first: 'Harry', last: 'Truman' };
const president = {
...firstLast,
...(hasMiddle ? { middel: 'S' } : {})
}
// 타입은
// {
// middel?: string | undefined;
// first: string;
// last: string;
// }
아래 코드의 경우 책에서는 유니온 타입으로 추론될 것으로 나온다. 그러나 4.1 버전부터 조건부 객체 전개는 선택적 속성을 만드는 것으로 변경되었다.
declare let hasDates: boolean;
const nameTitle = {name: 'Khufu', title: 'Pharaoh'};
const pharaoh = {
...nameTitle,
...(hasDates ? { start: -2589, end: -2566 } : {})
}
// v4.1 이전 유니온으로 타입 추론
{
start: number;
end: number;
name: string;
title: string;
} | {
name: string;
title: string;
}
// v4.1 이후 선택적 필드 방식으로 타입 추론
{
start?: number | undefined;
end?: number | undefined;
name: string;
title: string;
}
Conditional Spreads Create Optional Properties
별칭은 타입스크립트의 제어 흐름 분석과 타입 좁히기를 방해한다. 따라서 별칭은 일관성 있게 사용한다.
interface Coordinate {
x: number;
y: number;
}
interface BoundingBox {
x: [number, number];
y: [number, number];
}
interface Polygon {
exterior: Coordinate[];
holes: Coordinate[][];
bbox?: BoundingBox;
}
// 일관성 있게
// 1. 속성 체크에서도 별칭 사용하기
function isPointInPloygon(polygon: Polygon, pt: Coordinate) {
const box = polygon.bbox;
if(box) {
if (pt.x < box.x[0] || pt.x > box.x[1] || pt.y < box.y[0] || pt.y > box.y[1]) {
return false;
}
}
// ...
return true;
}
// 일관성 있게
// 2. 객체 비구조화로 일관된 이름 사용하기
function isPointInPloygon(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]) {
return false;
}
}
// ...
return true;
}
이때 비구조화 문법을 사용하여 일관된 이름을 사용할 수 있다.
타입스크립트는 함수가 타입 정제를 무효화하지 않는다고 가정한다. 그러나 실제로는 무효화될 가능성이 있다. 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 주의해야 한다.
이때 속성보다 지역 변수를 사용하면 타입 정제를 믿을 수 있다. 객체의 속성 값을 지역 변수로 뽑아내서 사용하면, 타입은 정확히 유지되면서 객체의 속성의 값과 같게 유지되지 않을 수 있다.
🌕 타입스크립트와 함께하는 풍요로운 한가위 되세요~ 🌕
const pt = {};
pt.x = 3; // {} 형식에 'x' 속성이 없습니다
변수의 값은 변경될 수 있지만 타입은 일반적으로 변경되지 않는다. 따라서 객체를 생성할 때는 속성을 하나씩 추가하는 것ㅂ도나는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다.
interface Point {
x: number,
y: number
}
const pt0 = {};
const pt1 = {...pt0, x: 3};
const pt:Point = {...pt1, y:4};
작은 객체들을 조합해서 큰 객체를 만들 경우에도 여러 단계를 거치는 것(Object.assign과 같은 메서드를 사용해서)은 좋지 않다. spread operator를 사용하면 큰 객체를 한번에 만들 수 있다.
전개 연산자를 사용하면 타입 걱정 없이 필드 단위로 객체를 생성할 수 있다. → 원하는 객체의 형식을 class처럼 지정해놓고 이를 전개 연산자로 풀어서 합치는 방법을 말하는듯?
declare let showAge: boolean;
const nameEmoji = {name: 'dory', emoji: '🐠' };
const aboutDory = {
...nameEmoji,
...(showAge ? { birth: 1998, age: 25 } : {})
}
안전한 방식으로 조건부 속성을 추가하려면 null또는 {} 객체 전개를 사용하면 된다.
declare const aboutDory: {
birth: number;
age: number;
name: string;
emoji: string;
} | {
name: string;
emoji: string;
};
declare const aboutDory: {
birth: number;
age: number;
name: string;
emoji: string;
} | {
name: string;
emoji: string;
};
전개 연산자로 한꺼번에 여러 속성을 추가할 수도 있다. 이 경우 조건문의 객체가 유니온 타입으로 추론된다. 이를 통해 유니온을 사용하는 것이 가능한 값의 집합을 더 정확하게 표현할 수 있음을 알 수 있다. (🚨주의🚨 v4.0.5까지만 이렇게 변환된다. 이후 버전부터는 아래처럼 선택적 필드로 변환된다. 이제는 아래와 같이만 쓰이니 주의하자!!!!)
declare const aboutDory: {
birth?: number | undefined;
age?: number | undefined;
name: string;
emoji: string;
};
타입스크립트는 런타임에 관계 없이 async/await를 사용할 수 있다.
Promsie.race도 타입 추론과 잘 맞는다. 타입 구문 없어도 반환 타입이 Promise
async 함수는 항상 프로미스를 반환하도록 강제한다.
const getNumber = async () => 42; // Promise<number>
사실상 즉시 사용 가능한 값이지만 async를 사용하면 항상 비동기로 실행된다. 함수를 사용할때 동기와 비동기가 섞여서는 안되는데 async는 코드 작성시 이를 도와준다. 콜백이나 프로미스를 사용하면 실수로 일부만 동기인 코드를 작성할 수 있지만, async를 사용하면 항상 비동기 코드를 작성하게 된다.
다들 즐추~~
const REQUIRES_UPDATE: {[k in keyof ScatterProps] : boolean} = {
xs: true,
ys: true,
xRange: true,
yRange: true,
color: true,
onClick: false,
};
function shouldUpdate(
oldProps: ScatterProps,
newProps: ScatterProps
){
let k : keyof ScatterProps;
for (k in oldProps){
if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){
return true;
}
}
}
객체 리터럴에 타입을 명시적으로 선언할 때
const elmon: Product = {
name :'Tickle Me Elmo',
id: '22222',
price: 28.99,
};
함수의 반환에도 타입을 명시하여 오류를 방지할 수 있다
function getQuote(ticker: string) : Promise<number> {
return fetch(`http://sdkfjsl')
.then(response => response.json());
}
const cache: {[ticker:string]; number} = {}
변수의 값은 바뀔 수 있지만 그 타입은 바뀌지 않는다
let id : string|number = 'dsfsdf';
하지만, 유니온 타입의 경우 매번 id가 어떤 타입인지 확인해야하기 때문에 문제가 발생할 수 있다
⇒ 차라리, 다른 변수를 도입하는 것이 나을 수 있다
다른 타입에 별도의 변수를 도입해야하는 이유
런타임에는 모든 변수는 유일한 값을 가진다. 하지만 타입스크립트가 작성한 코드를 체크하는 정적 분석 시점에 변수는 가능한 값들의 집합인 타입을 가지게 된다 변수를 초기화할 때 타입을 명시하지 않은 경우 이러한 타입을 가지고 타입 체커는 타입을 결정한다. 이러한 과정을 타입스크립트에서는 넓히기
라고 부른다.
const mixed = [’x’ , 1] 구문에서 추론될 수 있는 타입의 종류들
타입 넓히기를 제어할 수 있는 방법
타입 스크립트 기본 동작을 재정의 하는 방법
명시적 타입 구문 사용하기
const v : {x: 1|3|5} = {
x: 1,
}
타입 체커에 추가적인 문맥을 제공한다
const 단언문을 사용한다
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;}
분기문 사용하기
instacneof 사용하기
function contains(text: string, search:string|RegExp){
if (search instanceof RegExp){
search;
return !!search.exec(text)
}
}
속성 체크 하기
function pickAB(ab: A | B){
if ('a' in ab){
ab
}
}
태그를 붙이기
interface UploadEvent { type: 'upload'; filename:string contents:string}
interface DownloadEvent { type: 'download'; filename: string;}
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent){
switch(e.type){
case 'download' :
e // 타입은 DownloadEvent가 된다
break;
case 'upload' :
e // 타입이 UploadEvent가 된다
break;
}
}
타입을 좁힐 때에 주의해야 할 점
function addOptional<T extends object, U extends ojbect>(
a: T, b: U | null
): T & Partial<U>{
return { ...a, ...b };
}
const pharaoh = addOptional (
nameTItle,
hasDates ? {start: -2589, end: -2566} : null
)
const state = { location : 1 };
const loc = state.location; // 요게 별칭!
Promise<Promise<T>>
가 아니라, Promise<T>
가 된다.
type Language = 'JavaScript' | 'TypeScript';
function setLanguage(language: Language){}
setLanguage('JavaScript'); // 정상
let language = 'JavaScript'; setLanguage(language); // Error // 'string' 형식의 인수는 'Language' 형식의 매개변수에 할당 될 수 없습니다.
- why?
- 타입스크립트는 할당 시점에 타입을 추론한다.
- 따라서 string 으로 추론된것...
### 해결방법
1. 타입선언하기
```ts
let language:Language = 'JavaScript';
const language = 'JavaScript';
function panTo(where:[number,number]){}
panTo([10,20]); // 정상
conset loc = [10,20];
panTo(loc);
// number[] 형식의 인수는
// [number,number] 형식의 매개변수에 할당될 수 없습니다.
function panTo(where:[number,number]){}
conset loc = [10,20] as const;
panTo(loc);
// Error: readonly[10,20] 형식은
// 변경가능한 형식 [number,number] 에 할당 할 수 없습니다.
더도말고 덜도말고 타입스크립트만 같아라 타스와 함께 해피추석 스딸뜨