이펙티브 타입스크립트 스터디 - 3주차 발표 정리
주제: 3장 (타입 추론, 아이템 24~27)
아이템 24. 일관성 있는 별칭 사용하기
const target = { name: "target", location: [1, 1] };
const loc = target.location;
loc[0] = 2;
console.log(target.location); // [2, 1]
- 별칭의 값을 변경하면 원본 값의 속성도 변경 된다.
이 별칭을 남발하면 제어의 흐름을 추적하기가 힘들어진다.
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; // BoundingBox | undefined
const box = polygon.bbox;
box; // BoundingBox | undefined
if (polygon.bbox) {
polygon.bbox; // BoundingBox
box; // BoundingBox | undefined
}
}
TypeScript는 조건문 내부에서 변수가 특정 타입을 가지고 있다고 추론할 수 있다.
이를 타입 내로잉(type narrowing) 또는 타입 정제(type refinement)라고 한다
polygon.bbox의 타입을 정제하였지만, box는 그렇지 않아 나는 오류이다
=> 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 따라서 변수에 별칭을 사용할때는 일관되게 사용해야 한다.
const {bbox} = polygon;
if(bbox) {
const {x, y} = bbox;
...
}
사실 따로 별칭을 사용할 필요없이 그 값 그대로 사용하는 것도 방법이다.
하지만, 이 경우 x, y가 선택적 속성일 경우 추가적인 속성체크를 해줘야한다.
=> 비구조화 문법을 사용해서 일관된 이름을 사용하는 것이 좋다.
polygon.bbox // BoundingBox | undefined
if (polygon.bbox) {
polygon.bbox // BoundingBox
fn(polygon)
polygon.bbox // BoundingBox
}
fn 함수를 거쳤지만, 여전히 BoundingBox로 추론된다.
만약, fn 함수가 polygon.box 값을 변경시킬 수도 있다면?
차라리, BoundingBox | undefined 타입이 더 안전해 보이지만 타입스크립트는 함수 호출이 외부 상태를 변경할 가능성을 자동으로 고려하지 않는다.
만약 fn 함수가 다음 같다면?
function fn(polygon: Polygon) {
polygon.bbox = undefined; // bbox 값을 변경
}
따라서 이렇게 객체에서 값을 뽑아내서 사용할 때는 주의를 기울여야한다.
만약, bbox값으로 지역 변수를 선언했다면 더 정확한 타입을 유지할 수 있다.
fn(polygon);
if (polygon.bbox) {
// 함수 호출 후 다시 타입을 확인
console.log(polygon.bbox);
}
또는 정제를 한번 더하는 것도 방법이다.
=> 함수 호출이 객체 속성의 타입 정제를 무효화 할 수 있다는 점을 주의해야한다. 속성보다는 지역 변수를 사용하는 것이 타입 정의에 더 정확하다.
아이템 25. 비동기 코드에는 콜백 대신 async 함수 사용하기
콜백 지옥을 극복하기 위해 나온 프로미스은 타입스크립트에서도 유용하게 쓰인다.
const page1Promise = fetch(url1)
page1Promise
.then(response1 => {
return fetch(url2)
})
.then(response2 => {
return fetch(url3)
})
.then(response3 => {
// ...
})
.catch(error => {
// ...
})
- 실행 순서가 코드의 순서와 동일하여 이해하기 편해졌다.
- 오류를 처리할 수 있다.
- Promise.all 같은 기능을 사용할 수 있음.
(하지만, then의 남발....)
콜백보다는 프로미스가 코드를 작성하기도 쉽고, 타입을 추론하기 쉽다.
=> 콜백보다는 프로미스를 사용하는 게 코드 작성과 타입 추론면에서 유리하다.
async function fetchPages() {
const response1 = await fetch(url1)
const response2 = await fetch(url2)
const response3 = await fetch(url3)
// ...
}
이후 프로미스보다 더 효율적으로 처리할 수 있는 async/await이 등장했다.
try ~ catch 문을 통해 핸들링도 가능하고 타입스크립트는 런타임에 관계없이 async/await을 사용할 수 있다.
async/await은 다음과 같은 이점이 있다.
- 일반적으로 더 간결하고 직관적인 코드가 된다.
- async 함수는 항상 프로미스를 반환하도록 강제된다.
따라서, 웬만하면 프로미스를 직접 생성하기보다 async/await을 사용하자.
=> 가능하면 프로미스를 생성하기보다는 async와 await을 사용하는 것이 좋다.
함수는 항상 동기 또는 항상 비동기로 실행되어야 하며, 절대 혼용해서 사용하면 안된다.
콜백이나 프로미스를 사용하면 반동기 코드를 작성할 수 도 있지만,
async를 사용하면 항상 비동기 코드를 작성하는 셈이다.
반환 타입은 Promise<Promise<T>>가 아닌 Promise<T>이다.
async/await을 사용하면 타입 추론이 더 명확해지고. 이는 특히 복잡한 비동기 로직을 다룰 때 큰 이점이 된다.
=> 어떤 함수가 프로미스를 반환한다면, async로 선언하는 것이 좋다.
아이템 26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기
type Language = 'JavaScript' | 'TypeScript' | 'Python'
function setLanguage(language: Language) {
/* ... */
}
setLanguage('JavaScript') // 정상
let language = 'JavaScript'
setLanguage(language) // string 형식의 인수는 'Language' 형식의 매개변수에 할당될 수 없습니다.
export default {}
값을 변수로 분리해내면, 타입스크립트는 할당 시점에 타입을 추론한다. (위에선 string으로 추론됨)
해결법
1. 타입 선언시 가능한 값을 제한
let language: Language = 'JavaScript'
setLanguage(language)
2. 상수로 만들기
const language = 'JavaScript'
setLanguage(language)
튜플 사용 시 주의점
function panTo(where: [number, number]) {
/* ... */
}
panTo([10, 20]) // 정상
const loc = [10, 20]
panTo(loc) // number[] 형식의 인수는 [number, number] 형식의 매개변수에 할당될 수 없다.
const를 사용했음에도 길이를 알 수 없는 숫자의 배열로 추론됨.
[number, number] 타입과 number[] 타입의 차이는 고정된 길이가 있느냐의 차이이다.
const를 통해 숫자 배열을 변수에 할당하면 TypeScript는 기본적으로 loc를 number[] 타입으로 추론합니다.
해결법
1. 타입 선언
const loc: [number, number] = [10, 20]
panTo(loc) // 정상
const loc = [10, 20] as [number, number];
panTo(loc) // 정상
2. 상수 문맥 제공
const loc = [10, 20] as const
panTo(loc) // [10, 20] 형식은 readonly이며 변경 가능한 형식 [number, number]에 할당할 수 없음
단언만 하면 에러가 난다.
as const를 사용하면 TypeScript는 배열을 불변(readonly) 튜플로 간주한다. (readonly 속성은 배열 요소를 변경할 수 없도록 한다.)
반면, panTo 함수의 매개변수 타입 [number, number]는 변경 가능한 튜플을 기대한다.
따라서 불변 튜플을 변경 가능한 튜플로 전달할 때 타입 에러가 발생하게 된다.
function panTo(where: readonly [number, number]) {
/* ... */
}
const loc = [10, 20] as const
panTo(loc) // 정상
따라서, 위처럼 선언해주면 에러가 사라지게 된다.
다만 as const 의 단점은 타입 정의에 실수가 존재할 시에 오류가 타입 정의가 아니라 호출되는 곳에서 발생한다는 점이다.
객체 사용 시 주의점
type Language = 'JavaScript' | 'TypeScript' | 'Python'
interface GovernedLanguage {
language: Language
organization: string
}
function complain(language: GovernedLanguage) {
/* ... */
}
complain({ language: 'TypeScript', organization: 'Microsoft' }) // 정상
const ts = {
language: 'TypeScript',
organization: 'Microsoft',
}
complain(ts) // 에러
ts의 language와 organization은 string으로 추론된다.
이또한 위처럼 타입 선언을 추가하거나 상수 단언으로 해결한다.
아이템 27. 함수형 기법과 라이브러리로 타입 흐름 유지하기
로대시 같은 라이브러리들의 일부 기능(map, flatMap, filter, reduce ...)은 순수 JS로 구현되어 있음
이것들은 타입스크립트와 조합하면 더 빛을 낸다.
타입 정보가 그대로 유지되면서 타입 흐름이 계속 전달되도록 하기 때문이다.
const salaries = _.map(allPlayers, 'salary') // number[]
const teams = _.map(allPlayers, 'team') // string[]
const mix = _.map(allPlayers, Math.random() < 0.5 ? 'name' : 'salary') // (string | number)[]
새로운 타입으로 안전하게 반환
// lodash-es.d.ts 파일의 일부 예시
interface LoDashStatic {
map<T, TResult>(
collection: List<T> | null | undefined,
iteratee: ListIteratee<T, TResult>
): TResult[];
}
이 타입 선언은 map 함수가 입력 컬렉션의 요소 타입 T와 반환 요소 타입 TResult를 추론할 수 있음을 나타낸다.
이를 통해 입력과 출력의 타입이 자동으로 결정되며, 타입스크립트는 타입 일관성을 유지할 수 있게 된다.
내장된 함수형 기법과 로대시 같은 유틸리티 라이브러를 사용해서 구현하는 것이 직접 구현하는 것보다 좋은 이유
1. 타입 흐름 개선
2. 가독성 높임
3. 명시적인 타입 구문의 필요성을 줄임 (직접 구현하면 타입 체크도 직접 관리해야함.)
'프로그래밍언어 > TypeScript' 카테고리의 다른 글
이펙티브 타입스크립트 스터디 - 7주차 발표 정리 (0) | 2024.06.22 |
---|---|
이펙티브 타입스크립트 스터디 - 6주차 발표 정리 (1) | 2024.06.16 |
이펙티브 타입스크립트 스터디 - 5주차 발표 정리 (1) | 2024.06.07 |
이펙티브 타입스크립트 스터디 - 4주차 발표 정리 (0) | 2024.06.01 |
이펙티브 타입스크립트 스터디 - 1주차 발표 정리 (0) | 2024.05.11 |