Reputation: 513
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!
Upvotes: 2
Views: 201
Reputation: 329773
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.
Upvotes: 1