이펙티브 타입스크립트 스터디 - 4주차 발표 정리
주제: 4장 (타입 설계, 아이템 28~32)
아이템 28. 유효한 상태만 표현하는 타입을 지향하기
효과적으로 타입을 설계하려면, 유효한 상태만 표현할 수 있는 타입을 만들어내는 것이 가장 중요하다.
interface State {
pageText: string
isLoading: boolean
error?: string
}
function renderPage(state: State) {
if (state.error) {
return `Error! Unable to load ${currentPage}: ${state.error}`
} else if (state.isLoading) {
return `Loading ${currentPage}...`
}
return `<h1>${currentPage}</h1>\n${state.pageText}`
}
위 코드에서 isLoading이 true이면서 error 값이 존재한다면 어떻게 판단할까?
이때는 로딩중일까 오류일까?
async function changePage(state: State, newPage: string) {
state.isLoading = true
try {
const response = await fetch(getUrlForPage(newPage))
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`)
}
const text = await response.text()
state.isLoading = false
state.pageText = text
} catch (e) {
state.error = '' + e
}
}
위 코드의 문제점은 무엇일까?
1. 오류가 발생했을때 state.isLoading은 여전히 true이다.
2. state.error를 초기화하지 않음.
3. 페이지 로딩 중에 사용자가 페이지를 바꾼다면?
코드 작성시 주의점.
상태 값의 두 가지 속성이 동시에 정보가 부족하거나 서로 충돌할 수 있다는 문제점을 생각해보자.
개선.
interface RequestPending {
state: 'pending'
}
interface RequestError {
state: 'error'
error: string
}
interface RequestSuccess {
state: 'ok'
pageText: string
}
type RequestState = RequestPending | RequestError | RequestSuccess
interface State {
currentPage: string
requests: { [page: string]: RequestState }
}
function renderPage(state: State) {
const { currentPage } = state
const requestState = state.requests[currentPage]
switch (requestState.state) {
case 'pending':
return `Loading ${currentPage}...`
case 'error':
return `Error! Unable to load ${currentPage}: ${requestState.error}`
case 'ok':
return `<h1>${currentPage}</h1>\n${requestState.pageText}`
}
}
async function changePage(state: State, newPage: string) {
state.requests[newPage] = { state: 'pending' }
state.currentPage = newPage
try {
const response = await fetch(getUrlForPage(newPage))
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`)
}
const pageText = await response.text()
state.requests[newPage] = { state: 'ok', pageText }
} catch (e) {
state.requests[newPage] = { state: 'error', error: '' + e }
}
}
- 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발한다
- 유효한 상태만 표현하는 타입을 지향해야 한다.
아이템 29. 사용할 때는 너그럽게, 생성할 때는 엄격하게
함수의 매겨변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 일반적으로 타입의 범위가 더 구체적이어야 한다.
interface CameraOptions {
center?: LngLat
zoom?: number
bearing?: number
pitch?: number
}
type LngLat = { lng: number; lat: number } | { lon: number; lat: number } | [number, number]
type LngLatBounds = { northeast: LngLat; southwest: LngLat } | [LngLat, LngLat] | [number, number, number, number]
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;
.
.
.
const camera = viewportForBounds(bounds)
setCamera(camera)
const {
center: { lat, lng },
zoom,
} = camera
// ~~~ Property 'lat' does not exist on type ...
// ~~~ Property 'lng' does not exist on type ...
viewportforBounds의 문제점은 만들어질 때 너무 자유롭게 만들어진다는 점이다.
이처럼 반환 타입의 범위가 넓으면 어떤 타입이 반환될지 모르기 때문에 불편해진다.
사용하기 좋은 API일수록 반환 타입은 엄격한 법이다.
interface LngLat {
lng: number
lat: number
}
type LngLatLike = LngLat | { lon: number; lat: number } | [number, number]
interface Camera {
center: LngLat
zoom: number
bearing: number
pitch: number
}
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
center?: LngLatLike
}
type LngLatBounds =
| { northeast: LngLatLike; southwest: LngLatLike }
| [LngLatLike, LngLatLike]
| [number, number, number, number]
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
선택적 속성과 유니온 타입은 반환 타입보다 매개변수 타입에 사용하는 것이 더 일반적이다.
아이템 30. 문서에 타입 정보를 쓰지 않기
타입스크립트의 타입 시스템은 명확하기 때문에 때로는 주석을 작성하는 것보다 더 나은 방법일 수 있다.
주석은 누군가 강제하지 않으면 코드와 동기화되지 않지만, 타입스크립트는 타입 체커가 타입 정보를 동기화하도록 강제한다.
값을 변경하지 않음, 매개변수를 변경하지 않음 => readonly로 설명 대체
ageNum => age: number
단, 단위가 있는 숫자들은 예외이다.
timeMs는 time보다 더 명확하다.
아이템 31. 타입 주변에 null 값 배치하기
strictNullChecks 설정을 true로.
function extent(nums: number[]) {
let min, max
for (const num of nums) {
if (!min) {
min = num
max = num
} else {
min = Math.min(min, num)
max = Math.max(max, num)
// ~~~ 'number | undefined' 형식의 인수는
// 'number' 형식의 매개변수에 할당될 수 없습니다.
}
}
return [min, max]
}
const [min, max] = extent([0, 1, 2])
const span = max - min
// ~~~ ~~~ 개체가 'undefined'인 것 같습니다.
- extent([0, 1, 2])의 결과는 [0, 2]가 아니라 [1, 2]
- nums 배열이 비어있다면 [undefined, undefined]
개선.
function extent(nums: number[]) {
let result: [number, number] | null = null
for (const num of nums) {
if (!result) {
result = [num, num]
} else {
result = [Math.min(num, result[0]), Math.max(num, result[1])]
}
}
return result
}
아이템 32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
interface Layer {
type: 'fill' | 'line' | 'point'
layout: FillLayout | LineLayout | PointLayout
paint: FillPaint | LinePaint | PointPaint
}
interface FillLayer {
type: 'fill'
layout: FillLayout
paint: FillPaint
}
interface LineLayer {
type: 'line'
layout: LineLayout
paint: LinePaint
}
interface PointLayer {
type: 'paint'
layout: PointLayout
paint: PointPaint
}
type Layer = FillLayer | LineLayer | PointLayer
이렇게 변경해야지 잘못된 조합으로 섞이는 것을 방지할 수 있다.
또한, type 값을 조건문으로 걸고 타입의 범위를 좁힐 수 있다.
제어의 흐름을 분석할 수 있도록 태그를 넣는 것을 추천한다.
interface Person {
name: string
// 두 타입은 동시에 존재하던가, 동시에 없어야 한다.
placeOfBirth?: string
dateOfBirth?: Date
}
개선.
interface Person {
name: string
birth?: {
place: string
date: Date
}
}
앞으로는 birth 값 하나만 체크하면되므로 더 효율적이다.
'프로그래밍언어 > TypeScript' 카테고리의 다른 글
이펙티브 타입스크립트 스터디 - 7주차 발표 정리 (0) | 2024.06.22 |
---|---|
이펙티브 타입스크립트 스터디 - 6주차 발표 정리 (1) | 2024.06.16 |
이펙티브 타입스크립트 스터디 - 5주차 발표 정리 (1) | 2024.06.07 |
이펙티브 타입스크립트 스터디 - 3주차 발표 정리 (0) | 2024.05.25 |
이펙티브 타입스크립트 스터디 - 1주차 발표 정리 (0) | 2024.05.11 |