Skip to content

Latest commit

 

History

History
187 lines (146 loc) · 12.3 KB

hide-unsafe-casts.md

File metadata and controls

187 lines (146 loc) · 12.3 KB

Item 45: Hide Unsafe Type Assertions in Well-Typed Functions

Things to Remember

  • 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.

Code Samples

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


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

💻 playground


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


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


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

💻 playground


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


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


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


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


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


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

💻 playground


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