programming.protips.wiki
githubtwitter
/dont-outsmart-typechecker
#types

Don't try and outsmart the typechecker

When the type checker (e.g. Flow or TypeScript) complains about an issue, try and fix the underlying issue first. Ignore directives should be used sparingly - as a last resort.

Consider the following express middleware to log how long the logic layer in a request took:

Bad:

app.use(function (req, res, next) {
  const { logicStart, logicEnd } = res.locals;

  // $FlowFixMe: logicStart and logicEnd are numbers
  const logicLayerTime = logicEnd - logicStart;
  console.log(`Time take for the logic layer: ${logicLayerTime}`);

  next();
});

Good:

app.use(function (req, res, next) {
  const { logicStart, logicEnd } = res.locals;

  assert(typeof logicStart === 'number');
  assert(typeof logicEnd === 'number');

  const logicLayerTime = logicEnd - logicStart;
  console.log(`Time take for the logic layer: ${logicLayerTime}`);

  next();
});

Our middleware has an implicit dependency on someone else, somewhere else, in the application setting values for res.locals.logicStart and res.locals.logicEnd.

If for whatever reason we never set those values (i.e. a bug), both values be null. Which is particularly nasty for us, because when we come to do our calculation for logging, null - null coerces to 0! Which means we'd happily continue, logging 0 as a 'valid' value for the time taken.

We'd never get an error, and it would be hard to notice this! 😱

The type checker is generally much smarter than us dumb humans :)

Why

When the type checker complains about an issue, it's probably correct. In most cases, it's warning you about a potential runtime error. In this case, perhaps the movie hasn't been released yet, and therefore hasn't been assigned a rating.

In general, we can't "trust" data that comes from "the outside world" (the network, parsing files on disk etc). Using guards or schemas may help here.

In other cases, the type checker may complain due to some configuration error, or it can't find the types for an imported module. In such cases, it's usually worth spending some time trying to debug the issue. Popular type checkers such as Flow and TypeScript are well battle-tested - most error scenarios you could run into have probably been discussed before. You may be able to solve it with a quick googling of the stack trace, searching StackOverflow etc - and avoid the nasty @ts-ignore!

More examples

Bad Example

type Movie = {
  title: string;
  releaseDate: Date;
  rating: number | null;
};

async function getMovieRating(id: number): Promise<number> {
  const details: Movie = await fetchMovieDetails(id);

  // @ts-ignore: rating will always be not null!
  return details.rating;
}

Prefer

type Movie = {
  title: string;
  releaseDate: Date;
  rating: number | null;
};

async function getMovieRating(id: number): Promise<number> {
  const details: Movie = await fetchMovieDetails(id);

  if (!details.rating) {
    throw new Error('Movie does not yet have a rating!');
  }

  return details.rating;
}

If there really is a bad definition from a library that needs fixing or updating, consider pulling the new version of the library or attempting the fix.

If that's something you want to punt on for later, make a ticket or GitHub issue that includes a sample stack trace, and link to it in the comment so other folks can understand why the ignore comment is being used, and when it can be removed.

e.g.

// $FlowFixMe: MYJIRA-1234 foo exists in the bar object, but the typedefs for MyBarLibrary aren't up to date yet.
const { foo } = bar;