To summarize, the following types seem to be compatible with (IB) => void:

() => IB

(IB) => IB

() => void

No parameter at all

The nitty-gritty of TypeScript’s type system

In a more strongly typed language like C#, it’s clear that none of this would fly. But this is TypeScript, which defines its typing model on compatibility with the dynamic language JavaScript.

It almost looks like the type of the lambda isn’t part of the type signature of the method, which came as a quite a surprise to me (and also to my colleague, Urs, who is much more of a TypeScript expert than I am).

But maybe we don’t know enough about the TypeScript type system. Let’s look at the Type compatibility documentation for TypeScript.

This section starts off with a “Note on Soundness”, which contains a note that suggests that what we have above is completely valid TypeScript.

“The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we’ll explain where these happen and the motivating scenarios behind them.”

The section Comparing two functions starts off explaining some rather surprising things about the type-compatibility of functions: for a function to be type-compatible with another function, the types of its parameters must match the types of the target type’s parameters, but the number of parameters doesn’t have to match. So if the target type has 4 parameters and the lambda to assign has 0 parameters, that lambda is compatible.

Reëxamining the oddly compatible lambdas

Armed with this new knowledge, let’s see if the previously bizarre-seeming behavior is actually valid.

To recap, the TypeScript compiler says that following signatures are compatible with (IB) => void:

f(() => IB): IA: this is compatible because the zero parameters conform by definition and any return type is OK because void is expected.

f((IB) => IB): IA: this is compatible because the single parameter conforms and any return type is OK because void is expected.

f(() => void): IA: this is compatible because because the zero parameters conform by definition and any return type is OK because void is expected.

f() => IA: this one looks plain wrong at first, but the same logic applies to the whole function f((IB) => void) => IA instead of to the lambda parameter for it. The interface expects a function f with a single parameter, returning IA. By the first rule above, a function with zero parameters satisfies that requirement.

f((number) => void): IA: This does not satisfy the requirement because number is not compatible with IB.

f(number): IA: This does not satisfy the requirement because number is not compatible with (IB) => void.

f(): void: This does not satisfy the requirement because while zero parameters is OK, the type void is smaller than IA.

Well, it looks like there’s nothing to see here, folks. The compiler is doing exactly what it’s supposed to. Move along and get on with your day.

Unfortunately, that means that TypeScript is going to be considerably less helpful for ensuring program correctness than I’d previously thought.

In fact, the caveat about Typescript “allow[ing] unsound behavior [in] carefully considered [places]” seems a bit disingenuous because, to a programmer accustomed to something like C# or Java or Swift, this kind of type-enforcement for method compatibility cannot be relied upon to enforce much of anything.

Actual vs. Formal Arguments

When I read OOSC2 (Amazon) a long time ago[1], I remember how Bertrand Meyer made the distinction between the formal type of an argument (the type in the method signature) and the actual type of an argument (the runtime type).

The method-type–conformance rules for TypeScript make sense for actual arguments. They ensure compatibility with JavaScript. What’s not clear to me is that this same logic be applied to formal arguments that are only available in TypeScript. If I declare a specific type signature in an interface, what are the odds that I want the wishy-washy JavaScript-friendly type rules for those situations? From an architect’s point of view, it would certainly be nicer to have more strict type-checking for formal definitions.

Since we don’t have that, this very lenient type-compatibility renders type-checking for lambdas largely useless in interface declarations. The compiler won’t be able to tell you that your implementation no longer matches the interface declaration because almost anything you write will actually match.