Type safety of lambdas in TypeScript

October 27th 2023 TypeScript

Recently, I've been adding some GraphQL queries to a legacy web application in TypeScript. With GraphQL, you quickly end up with a lot of similar types, since the query determines which fields are present in a type. I was relying on the TypeScript compiler to warn me of resulting potential type incompatibilities, and was surprised when it quietly allowed a lambda with a subtype parameter:

interface Book {
  author: string;
  title: string;
}

interface PrintedBook extends Book {
  pages: number;
}

const books: Array<Book> = [
  {
    author: "Mark J. Price",
    title: "Apps and Services with .NET 7",
  },
  {
    author: "Carl-Hugo Marcotte",
    title: "An Atypical ASP.NET Core 6 Design Patterns Guide",
  },
];

const longBooks = books.filter((book: PrintedBook) => book.pages > 700);

I was expecting the code above to result in an error because the Book instances in the books array are declared to not have a pages field, which is required by the lambda in the filter method.

Sure, you could always use implicit types for the lambda parameters:

const longBooks = books.filter((book) => book.pages > 700);

And get a compiler error as expected:

Property pages does not exist on type Book.

But I was still disappointed that the original code didn't also cause a compiler error. So, I decided to investigate further.

Pretty soon, I noticed that the project didn't have the strict flag enabled in tsconfig.json. As soon as I enabled it, I got the compiler error I wanted:

Property pages is missing in type Book but required in type PrintedBook.

However, the flag was disabled for a reason. The large code base was created before its time, and enabling it would mean having to address many errors. Doing that could easily introduce subtle bugs in the code and would require extensive additional testing.

If you are in a similar situation, there is fortunately a more specific flag which you can enable to get errors when using subtypes for lambda parameters: the strictFunctionTypes flag. It is likely to cause a lot less errors in an old code base, which should also be less risky to fix.

The reasoning why this check is being performed when one of the above-mentioned strict type checking flags is enabled is well documented in the TypeScript reference. I recommend reading it if you are at least a bit curious.

To try out the described behavior yourself, you can download sample code from my GitHub repository. Individual commits have different compiler flags enabled so that you can easily switch between them.

In its current version, the TypeScript compiler does very strict type checking, as the strict flag is enabled by default. Once you get used to that, you can easily forget that this wasn't always the case. Older projects are likely to have all or at least some of the strict type checking disabled, which can be potentially dangerous if you are used to relying on the compiler to write type safe code.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License