TypeScript Interface Function Field Contravariance Type Error

Viewed 150

I have the following type, which establishes that all properties are going to be functions, which accept either no arguments, or a single argument of type Record<string, any>:

type FnTrait = Record<
  string,
  (input?: Record<string, any>) => any
>;

I attempt extend this type into another interface (which I want to have the same constraint).

interface HasFn extends FnTrait {
  someFn(): string; // no problem
  anotherFn(o: {id: string}): number; // ts error 2411
}

This produces an error on anotherFn: Property 'anotherFn' of type '(o: { id: string; }) => number' is not assignable to string index type '(input?: Record<string | number | symbol, any> | undefined) => any'.

Why is it that someFn produces no error, while anotherFn produces ts error 2411? It seems that this narrowing should be allowed.

Any help would be greatly appreciated. Thank you!

1 Answers

This is an instance of function types being contravariant in their parameters. The word "contravariant" means "varies in the opposite way". If you make a function's parameter more specific (narrow), you are making the function type itself more general (wide). That means instead of making HasFn a subtype of FnTrait (which is what extends means), you are sort of making it a supertype. This is not allowed, and violates the principle of substitutability.

In particular, anotherFn() is in error because it requires that its argument have a string-valued id property, while FnTrait["anotherFn"] does not. It is expected that you can call any property of a FnTrait with either no parameters, or with a single parameter of just about any type. But a HasFn might explode if you call its anotherFn() method without a parameter, or with a parameter missing the right sort of id property. Therefore, as defined, HasFn is not assignable to FnTrait, despite being declared to extend it:

const hasFn: HasFn = {
  someFn: () => "",
  anotherFn: o => o.id.length
}
const fnTrait: FnTrait = hasFn;
fnTrait.anotherFn({ a: 123 }); // okay at compile time, explodes at runtime

Since anotherFn() means you can't safely substitute a FnTrait value where a HasFn value is requested, FnTrait fails to be assignable to HasFn, and you get an error.


The reason why someFn() is not in error is because a function of fewer parameters is assignable to a function that takes more parameters. This is because someFn() will, by necessity, ignore any parameters passed into it, so it is safe to treat it as a function which might possibly receive a parameter:

fnTrait.someFn({ a: 123 }); // okay at compile time and runtime

This works for the same reason anotherFn() fails: substitutability.

Playground link to code

Related