Skip to content

Instantly share code, notes, and snippets.

@quicksnap
Last active August 18, 2022 22:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quicksnap/07f7d8dc3d2669d498201e1967774bbc to your computer and use it in GitHub Desktop.
Save quicksnap/07f7d8dc3d2669d498201e1967774bbc to your computer and use it in GitHub Desktop.

Avoid TypeScript as assertions! Especially with return values...

Note: A satisfies operator may improve this issue. A detailed discussion of this is here: microsoft/TypeScript#47920

Using as in TypeScript is usually bad, since it can downcast your type. Example:

type Dog = { name: string; breed: string };
const puppy = { name: 'Spot' } as Dog;

// Uh oh, runtime error! `puppy.breed` is undefined.
console.log(puppy.breed.toUpperCase());

With { name: 'Spot' } as Dog, TypeScript allows you to downcast to Dog even though it is missing the breed property. This is by design: using an as assertion is an escape hatch that tells TypeScript you know what you're doing. The compiler will do its best to prevent impossible assertions, such as const myString = 123 as string;, but downcasts are fair game.

Here's the better way:

type Dog = { name: string; breed: string };

// TS Error: Property 'breed' is missing in type '{ name: string; }' but required in type 'Dog'
const puppy: Dog = { name: 'Spot' };

This is an improvement; we prefer compiler errors over runtime errors.

Using const puppy: Dog = { name: 'Spot' } leverages a type annotation. This strictly enforces the type, and also has the benefit here of enforcing freshness.

Function returns

I see as being used commonly when returning values from a function:

function puppyTime() {
  return { name: 'Spot', breed: 'Cute' } as Dog;
}

This suffers from the drawbacks mentioned above. This is problematic when the type of Dog changes in the future. If we were to add age: number to Dog someday, puppyTime() would return an incomplete value and potentially cause runtime errors.

Instead, use a return value annotation:

function puppyTime(): Dog {
  return { name: 'Spot', breed: 'Cute' };
}

This will safeguard your code for future type changes.

map(), reduce() and other inferred values

Here's some examples for map and reduce. Other utilities should offer you generics to allow for function return inference. If not, you can annotate the return value yourself instead of as assertion.

const numbers = [1, 2, 3];

// Don't do this:
numbers.map(num => {
  return { name: `Puppy #${num}`, breed: 'V cute' } as Dog;
});

// Do this:
numbers.map<Dog>(num => {
  return { name: `Puppy #${num}`, breed: 'V cute' };
});

// Don't do this:
numbers.reduce((acc, cur) => {
  return { ...acc, [`dog-${cur}}`]: { name: `Puppy #${cur}`, breed: 'V cute' } } as Record<string, Dog>;
}, {});

// Do this:
numbers.reduce<Record<string, Dog>>((acc, cur) => {
  return { ...acc, [`dog-${cur}}`]: { name: `Puppy #${cur}`, breed: 'V cute' } };
}, {});

Thank you for coming to my TED talk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment