Item 45: Hide Unsafe Type Assertions in Well-Typed Functions
要点
- Sometimes unsafe type assertions and
any
types are necessary or expedient. When you need to use one, hide it inside a function with a correct signature. - Don't compromise a function's type signature to fix type errors in the implementation.
- Make sure you explain why your type assertions are valid, and unit test your code thoroughly.
- 有时不安全的类型断言和
any
类型是必要的或是为了加快开发进度。当你需要使用它们时,将其隐藏在具有正确签名的函数内部。 - 不要为了修复实现中的类型错误而妥协函数的类型签名。
- 确保解释为什么你的类型断言是有效的,并且对代码进行充分的单元测试。
正文
ts
interface MountainPeak {
name: string
continent: string
elevationMeters: number
firstAscentYear: number
}
async function checkedFetchJSON(url: string): Promise<unknown> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Unable to fetch! ${response.statusText}`)
}
return response.json()
}
export async function fetchPeak(peakId: string): Promise<MountainPeak> {
return checkedFetchJSON(`/api/mountain-peaks/${peakId}`)
// ~~~~~ Type 'unknown' is not assignable to type 'MountainPeak'.
}
ts
export async function fetchPeak(peakId: string): Promise<unknown> {
return checkedFetchJSON(`/api/mountain-peaks/${peakId}`) // ok
}
ts
const sevenPeaks = [
'aconcagua',
'denali',
'elbrus',
'everest',
'kilimanjaro',
'vinson',
'wilhelm',
]
async function getPeaksByHeight(): Promise<MountainPeak[]> {
const peaks = await Promise.all(sevenPeaks.map(fetchPeak))
return peaks.toSorted(
// ~~~ Type 'unknown' is not assignable to type 'MountainPeak'.
(a, b) => b.elevationMeters - a.elevationMeters
// ~ ~ 'b' and 'a' are of type 'unknown'
)
}
ts
async function getPeaksByDate(): Promise<MountainPeak[]> {
const peaks = (await Promise.all(sevenPeaks.map(fetchPeak))) as MountainPeak[]
return peaks.toSorted((a, b) => b.firstAscentYear - a.firstAscentYear)
}
ts
export async function fetchPeak(peakId: string): Promise<MountainPeak> {
return checkedFetchJSON(
`/api/mountain-peaks/${peakId}`
) as Promise<MountainPeak>
}
ts
async function getPeaksByContinent(): Promise<MountainPeak[]> {
const peaks = await Promise.all(sevenPeaks.map(fetchPeak)) // no assertion!
return peaks.toSorted((a, b) => a.continent.localeCompare(b.continent))
}
ts
export async function fetchPeak(peakId: string): Promise<MountainPeak> {
const maybePeak = checkedFetchJSON(`/api/mountain-peaks/${peakId}`)
if (
!maybePeak ||
typeof maybePeak !== 'object' ||
!('firstAscentYear' in maybePeak)
) {
throw new Error(`Invalid mountain peak: ${JSON.stringify(maybePeak)}`)
}
return checkedFetchJSON(
`/api/mountain-peaks/${peakId}`
) as Promise<MountainPeak>
}
ts
export async function fetchPeak(peakId: string): Promise<MountainPeak>
export async function fetchPeak(peakId: string): Promise<unknown> {
return checkedFetchJSON(`/api/mountain-peaks/${peakId}`) // OK
}
const denali = fetchPeak('denali')
// ^? const denali: Promise<MountainPeak>
ts
function shallowObjectEqual(a: object, b: object): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== b[k]) {
// ~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
return false
}
}
return Object.keys(a).length === Object.keys(b).length
}
ts
function shallowObjectEqualBad(a: object, b: any): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== b[k]) {
// ok
return false
}
}
return Object.keys(a).length === Object.keys(b).length
}
ts
shallowObjectEqual({ x: 1 }, null)
// ~~~~ Type 'null' is not assignable to type 'object'.
shallowObjectEqualBad({ x: 1 }, null) // ok, throws at runtime
ts
function shallowObjectEqualGood(a: object, b: object): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== (b as any)[k]) {
// `(b as any)[k]` is OK because we've just checked `k in b`
return false
}
}
return Object.keys(a).length === Object.keys(b).length
}