※ 아이템23. 한꺼번에 객체 생성하기
변수값은 변경될 수 있지만 타입스크립트의 타입은 일반적으로 변경되지 않는다.
따라서 객체를 생성할 때는 속성을 하나씩 추가하기 보다는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다.
const pt = {}
pt.x = 3
// ~ Property 'x' does not exist on type '{}'
pt.y = 4
// ~ Property 'y' does not exist on type '{}'
pt 는 빈 객체로 생성되었기 때문에 위 방식으로 할당이 불가하다. 존재하지 않는 속성을 추가할 수 없다.
객체를 한번에 정의하면 문제를 해결할 수 있다.
interface Point {
x: number
y: number
}
const pt = {
x: 3,
y: 4,
} // OK
해결 ! 근데 만약 반드시 나눠서 만들어야 한다면 단언문을 사용할 수 있다.
const pt = {} as Point
pt.x = 3
pt.y = 4 // OK
작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우에도 여러 단계를 거치는 것은 좋지 않다.
const pt = { x: 3, y: 4 }
const id = { name: 'Pythagoras' }
const namedPoint = {}
Object.assign(namedPoint, pt, id)
namedPoint.name
// ~~~~ Property 'name' does not exist on type '{}'
'객체 전개 연산자' ... 를 사용하면 큰 객체를 한꺼번에 만들어 낼 수 있다.
const pt = { x: 3, y: 4 }
const id = { name: 'Pythagoras' }
const namedPoint = { ...pt, ...id }
namedPoint.name // OK, type is string
객체 전개 연산자란 ?
chatGPT 답변 :
객체 전개 연산자(Object spread syntax)는 JavaScript의 문법 중 하나입니다. 이 연산자는 주로 객체나 배열의 요소를 전개하여 복사하거나 합치는 데 사용됩니다.
객체 전개 연산자는 ...으로 표시되며, 객체나 배열을 전개할 때 사용됩니다. 객체 전개 연산자를 사용하면 객체의 모든 속성을 다른 객체에 복사하거나, 기존 객체에 새로운 속성을 추가할 수 있습니다.
다음은 객체 전개 연산자를 사용하여 두 개의 객체를 병합하는 간단한 예시입니다.
const obj1 = {a: 1, b: 2};
const obj2 = {c: 3, d: 4};
const obj3 = {...obj1, ...obj2};
console.log(obj3); // {a: 1, b: 2, c: 3, d: 4}
위의 코드에서 ... 연산자를 사용하여 obj1과 obj2의 속성을 모두 포함하는 새로운 객체 obj3을 만듭니다.
객체 전개 연산자는 객체 뿐만 아니라 배열에서도 사용할 수 있습니다. 배열에서 전개 연산자를 사용하면 배열의 요소를 다른 배열에 복사하거나, 새로운 요소를 추가할 수 있습니다. 다음은 배열 전개 연산자를 사용하는 간단한 예시입니다.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2];
console.log(arr3); // [1, 2, 3, 4, 5, 6]
위의 코드에서 배열 전개 연산자를 사용하여 arr1과 arr2의 모든 요소를 포함하는 새로운 배열 arr3을 만듭니다.
객체나 배열 전개 연산자는 코드를 간결하게 작성하고 가독성을 높일 수 있으며, 자바스크립트에서 매우 유용한 문법 중 하나입니다.
이런 객체 연산자를 사용하면 타입 걱정 없이 필드 단위로 객체를 생성할 수도 있다.
이때 모든 업데이트마다 새 변수를 사용해 각각 새로운 타입을 얻도록 하는 것이 중요하다.
interface Point {
x: number
y: number
}
const pt0 = {}
const pt1 = { ...pt0, x: 3 }
const pt: Point = { ...pt1, y: 4 } // OK
위 방법은 객체에 속성을 추가하고 타입스크립트가 새로운 타입을 추론할 수 있게 해 유용하다는데 특별히 유용한지 모르겠음
▶ 전개 연산자를 사용해 객체 생성하기
null 또는 {} 으로 객체 전개 사용하기
- 타입에 안전한 방식으로 조건부 속성을 추가할 수 있다.
declare let hasMiddle: boolean
const firstLast = { first: 'Harry', last: 'Truman' }
const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }
요약
- 속성을 제각각 추가하지 말고 한꺼번에 객체로 만들어야한다. 안전한 타입으로 속성을 추가하려면 객체 전개({...a,...b}) 를 사용하면 된다.
- 객체에 조건부로 속성을 추가하는 방법을 익혀야 한다.
※ 아이템24 일관성있는 별칭 사용하기
const borough = { name: 'Brooklyn', location: [40.688, -73.979] }
const loc = borough.location
borough.location 배열에 loc 이라는 별칭을 줬다.
별칭의 값을 변경하면 원래 속성값에서도 변경된다.
loc[0] = 0 //별칭 값 변경
console.log(borough.location) //[ 0, -73.979 ]
별칭을 남발하면 제어흐름을 분석하기 어렵다는 것을 알려주는 예제 : 다각형
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
}
}
// ... more complex check
}
polygon 과 Coordinate 타입의 pt 를 매개변수로 받아서
polygon.bbox 가 true 이면서
bbox 영역을 벗어난다면 false 를 응답한다.
요약하자면 isPointInPolygon 함수는 어떤 점이 polygon 안에있는 점인지, 밖에 있는 점인지를 판단하는 것이다.
이 코드는 타입체크도 통과하고 잘 동작하지만, 반복되는 부분이 존재한다 (polygon.bbox)
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) {
polygon.bbox // Type is BoundingBox | undefined
const box = polygon.bbox
box // Type is BoundingBox | undefined
if (polygon.bbox) {
polygon.bbox // Type is BoundingBox
box // Type is BoundingBox | undefined
}
}
polygon.bbox 의 값은 정제했지만 box 는 그렇지 않았기 때문에 오류가 발생했다.
polygon.bbox : BoundingBox
box : BoundingBox | undefined
별칭을 일관성 있게 사용해 위 오류를 제거해보자
function isPointInPolygon(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[1] ||
pt.y > box.y[1]) {
// OK
return false
}
}
// ...
}
이런 경우 타입체커 문제는 해결되었으나,
box 와 bbox 가 같은 값인데 다른 이름을 사용하고 있어 문제가 남아있다.
객체 비구조화를 사용해 코드를 변경해보자 (구조분해할당)
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 < x[0] ||
pt.y > y[1]) {
return false
}
}
// ...
}
const polygon: Polygon = { exterior: [], holes: [] }
function calculatePolygonBbox(polygon: Polygon) {
polygon.bbox = { x: [0,1], y: [2, 3]}
}
const { bbox } = polygon
if (!bbox) {
calculatePolygonBbox(polygon) // Fills in polygon.bbox
// Now polygon.bbox and bbox refer to different values!
console.log(polygon.bbox, bbox); //{ x: [ 0, 1 ], y: [ 2, 3 ] } undefined
}
polygon.bbox 는 calculatePolygonBbox 함수에 의해서 생김
근데 bbox 는 처음에 구조분해 할당되어있는 애는 (const { bbox } = polygon) 여전히 undefined
그니까 ! calculatePolygonBbox 함수에 의해생긴 polygon.bbox 는 값이 있고,
구조분해할당해서 생긴 bbox 는 undefined 인 것임
요약
- 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 따라서 변수에 별칭을 사용할 때는 일관되게 사용해야 한다.
- 비구조화 문법을 사용해서 일관된 이름을 사용하는 것이 좋다.
- 함수 호출이 객체 속성의 타입 정제를 무효화 할 수 있다는 점을 주의해야 한다. 속성보다 지역변수를 사용하면 타입 정제를 믿을 수 있다.
※ 아이템25 비동기 코드에는 콜백 대신 async 함수 사용하기
콜백지옥
// HIDE
function fetchURL(url: string, cb: (response: string) => void) {
cb(url)
}
const url1 = '1'
const url2 = '2'
const url3 = '3'
// END
fetchURL(url1, function (response1) {
fetchURL(url2, function (response2) {
fetchURL(url3, function (response3) {
// ...
console.log(1)
})
console.log(2)
})
console.log(3)
})
console.log(4)
// Logs:
// 4
// 3
// 2
// 1
실행의 순서는 코드의 순서와 반대이다.
이렇게 콜백이 중첩된 코드는 직관적으로 이해하기 어렵다.
const page1Promise = fetch(url1)
page1Promise
.then(response1 => {
return fetch(url2)
})
.then(response2 => {
return fetch(url3)
})
.then(response3 => {
// ...
})
.catch(error => {
// ...
})
코드의 중첩도 적어졌고 실행순서도 코드 순서와 같아졌다. Promise.all 같은 기법을 사용하기도 쉬워졌다.
Promise.all 이란 ?
chatGPT 답변 :
Promise.all()은 자바스크립트의 Promise 객체를 활용하는 함수 중 하나입니다. 이 함수는 여러 개의 Promise 객체를 받아들이고, 이들이 모두 완료되면 그 결과를 배열로 반환하는 Promise 객체를 반환합니다. 이 때, 각각의 Promise 객체는 병렬적으로 처리됩니다.
Promise.all() 함수는 매우 유용한 함수 중 하나입니다. 예를 들어, 여러 개의 서버로부터 데이터를 가져와야 하는 경우에, 각각의 서버에서 데이터를 가져오는 작업을 병렬적으로 처리하여 시간을 단축할 수 있습니다. 또한, 여러 개의 파일을 동시에 업로드하거나, 여러 개의 API 요청을 병렬적으로 처리해야 하는 경우에도 Promise.all() 함수를 사용할 수 있습니다.
Promise.all() 함수는 다음과 같은 방식으로 사용할 수 있습니다.
Promise.all([promise1, promise2, promise3])
.then(function(results) {
// 이곳에서 결과를 처리합니다.
})
.catch(function(error) {
// 이곳에서 에러를 처리합니다.
});
위의 코드에서, Promise.all() 함수는 promise1, promise2, promise3 세 개의 Promise 객체를 받아들입니다. 이들 Promise 객체는 병렬적으로 처리됩니다. 이들 중 어느 하나라도 실패하면, 전체 Promise 객체는 실패하게 됩니다. 그러나, 모든 Promise 객체가 성공하면, Promise.all() 함수는 모든 결과를 배열로 반환합니다. 이 배열은 then() 함수에서 받아들여서 결과를 처리하게 됩니다. 만약, Promise.all() 함수의 실행 도중 에러가 발생하면, catch() 함수에서 해당 에러를 처리합니다
ES2017 에서는 async 와 await 키워드를 도입해 콜백지옥을 간단하게 처리할 수 있게 되었다.
await 키워드 : 각각의 프로미스가 처리(resolve) 될 때까지 fetchPages 함수의 실행을 멈춘다. 그리고 async 함수 내에서 await 중인 프로미스가 거절(reject) 되면 예외를 던진다.
async function fetchPages() {
try {
const response1 = await fetch(url1)
const response2 = await fetch(url2)
const response3 = await fetch(url3)
// ...
} catch (e) {
// ...
}
}
타입스크립트는 런타임에 관계없이 async/await 를 사용할 수 있다.
왜 콜백보다 Promise 혹은 async/await 를 사용해야할까 ?
- 콜백보다는 프로미스가 코드를 작성하기 쉽다.
- 콜백보다는 프로미스가 타입을 추론하기 쉽다.
예를들어 아래에서 타입스크립트는 세가지 response 변수 타입을 Response 로 추론한다.
async function fetchPages() {
const [response1, response2, response3] = await Promise.all([fetch(url1), fetch(url2), fetch(url3)])
// ...
}
또한 입력된 프로미스들 중 첫번째가 처리될 때 완료되는 Promise.race 도 타입추론과 잘 맞는다.
Promise.race 이란 ?
chatGPT 답변 :
Promise.race()는 여러 개의 Promise 중 하나가 먼저 완료될 때까지 기다리고 그 결과를 반환하는 메서드입니다. 이 메서드는 Promise 배열을 인자로 받아 가장 먼저 완료된 Promise의 결과나 reject reason을 반환합니다.
예를 들어, 다음과 같은 세 개의 Promise가 있다고 가정해보겠습니다.
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'promise1 resolved');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'promise2 resolved');
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, 'promise3 resolved');
});
이제 Promise.race()를 사용하여 이들 중 가장 먼저 완료된 Promise를 찾아보겠습니다.
Promise.race([promise1, promise2, promise3])
.then(result => console.log(result))
.catch(error => console.log(error));
위의 코드에서는 Promise.race()에 세 개의 Promise를 배열로 전달합니다. 이 메서드는 배열의 첫 번째 Promise인 promise1이 1초 후에 먼저 완료되기 때문에 promise1 resolved를 콘솔에 출력하게 됩니다. Promise.race()는 다른 Promise가 완료될 때까지 기다리지 않고, 가장 먼저 완료된 Promise의 결과를 반환합니다.
따라서 Promise.race()는 여러 개의 Promise 중 하나만이 성공하면 되는 경우에 유용합니다. 예를 들어, 여러 개의 API 요청 중 가장 빠른 응답을 받아야 하는 경우나, 타임아웃 기능을 구현해야 할 때 등에 활용할 수 있습니다.
프로미스를 사용하면 타입스크립트의 모든 타입 추론이 제대로 동작한다. 아래 예제를 보자
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)])
}
타입 구문이 없어도 fetchWithTime 의 반환타입은 Promise<Response> 로 추론된다. 그 이유는 무엇일까 ?
Promise.race 의 반환타입 = Promise<Response | never> : 입력타입들의 유니온
never(공집합) 과의 유니온은 아무런 효과가 없다. 따라서 결과는 Promise<Response> 로 간단해진다.
일반적으로는 프로미스를 생성하기 보다는 async/await 를 사용해야 하는 이유는 다음과 같다
- 일반적으로 더 간결하고 직관적인 코드가 된다.
- async 함수는 항상 프로미스를 반환하도록 강제된다
요약
- 콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론 면에서 유리하다.
- 가능하면 프로미스를 생성하기보다는 async 와 await 를 사용하는 것이 좋다. 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수 있다.
- 어떤 함수가 프로미스를 반환한다면 async 로 선언한는 것이 좋다.
'Typescript' 카테고리의 다른 글
[Typescript] 이펙티브 타입스크립트 4 아이템28 (0) | 2023.03.16 |
---|---|
[Typescript] 이펙티브 타입스크립트 3 아이템21-22 (0) | 2023.03.13 |
[Typescript] 이펙티브 타입스크립트 3 아이템19-20 (0) | 2023.03.09 |
[Typescript] 이펙티브 타입스크립트 2장 아이템17-18 (0) | 2023.03.07 |
[Typescript] 이펙티브 타입스크립트 2장 아이템15-16 (0) | 2023.03.07 |