Type safety of lambdas in 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 typeBook
.
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 typeBook
but required in typePrintedBook
.
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.