Skip to content

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'.
}

💻 playground


ts
export async function fetchPeak(peakId: string): Promise<unknown> {
  return checkedFetchJSON(`/api/mountain-peaks/${peakId}`) // ok
}

💻 playground


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'
  )
}

💻 playground


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)
}

💻 playground


ts
export async function fetchPeak(peakId: string): Promise<MountainPeak> {
  return checkedFetchJSON(
    `/api/mountain-peaks/${peakId}`
  ) as Promise<MountainPeak>
}

💻 playground


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))
}

💻 playground


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>
}

💻 playground


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>

💻 playground


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
}

💻 playground


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
}

💻 playground


ts
shallowObjectEqual({ x: 1 }, null)
//                         ~~~~ Type 'null' is not assignable to type 'object'.
shallowObjectEqualBad({ x: 1 }, null) // ok, throws at runtime

💻 playground


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
}

💻 playground

Released under the MIT License.