In the codebase I'm working on, it makes sense to have a generalised type that includes a function with a parameter of type unknown. Then wherever that type is used, narrow the function's parameter type to something more specific. However, this raises an error like:
Type 'unknown' is not assignable to type Foo
Following is some simple code to illustrate what I'm trying to do:
interface Foo {
func: (arg0: unknown) => number;
}
type SolidType = {
someNumber: number;
}
interface Bar extends Foo {
func: (arg0: SolidType) => number;
}
const x: Bar = {
func: (arg0) => {
return arg0.someNumber;
}
}
The above code raises the following error:
Interface 'Bar' incorrectly extends interface 'Foo'.
Types of property 'func' are incompatible.
Type '(arg0: SolidType) => number' is not assignable to type '(arg0: unknown) => number'.
Types of parameters 'arg0' and 'arg0' are incompatible.
Type 'unknown' is not assignable to type 'SolidType'.ts(2430)
Why is it not possible to override an unknown with a specific type in this case? Am I using unknown incorrectly here, and if so is there a better alternative?
With the --strictFunctionTypes flag enabled, the compiler protects you against unsafe function types by checking their parameter types contravariantly, which means that a function type (a: X) => void extends (or "is assignable to" or "is a subtype of") a function type (a: Y) => void if and only if Y extends X. Note that the assignability direction changes for the function type compared to that of its parameter type. That is, function types vary counter to their parameter types. In other words, they contra-vary.
Why does type safety require this? It has to do with direction of data flow. When data flows from a source to a target, the type the source sends must extend the type the target accepts. The source can safely get narrower (e.g., "I claimed to give you a Fruit, and I'm actually giving you a Banana") and the target can safely get wider (e.g., "you gave me a Fruit, and I'd actually accept any Food whatsoever"). It is unsafe for the source to get wider (e.g., "I claimed to give you a Fruit, but I'm actually giving you some Food which may or may not be a Fruit, I don't know, sorry) or for the target to get narrower (e.g., "you gave me a Fruit, but I really only accept a Banana and maybe you gave me something else, so I'm not satisfied")
When you pass data into a function parameter, the function is receiving the data, and thus the parameter type can be safely widened, not narrowed.
Let's examine your case:
interface Foo {
func: (arg0: unknown) => number;
}
Here, Foo's func() method apparently, right off the bat, claims to accept an argument of type unknown. That means it is already as wide as it can possibly be. Callers of func() can pass in anything whatsoever that they like:
function takeFoo(foo: Foo) { foo.func("this is fine"); }
But your definition of Bar does this:
interface Bar {
func: (arg0: SolidType) => number;
}
Any value of type Bar is free to expect that its func() method will be called with a SolidType:
const x: Bar = {
func: (arg0) => {
return Number(arg0.someNumber.toFixed(2));
}
}
If Bar extends Foo were true, it would require that the following line should be just fine:
takeFoo(x) // <-- this should be allowed if Bar extends Foo
But of course at runtime, this would fail with arg0.someNumber is undefined.
By saying Bar extends Foo, you are unsafely narrowing the function parameter of func() from unknown ("I'll take anything!") to SolidType ("I lied when I said I'd take anything, sorry!").
So, how to fix it? Well, it really depends strongly on your use cases.
If you have a Foo that isn't known to be of some specific intended subtype like Bar, will you ever call its func() method? Do you really plan on supporting foo.func("this is fine") and foo.func(123) and foo.func(new Date())? I'm guessing not, and that you will only actually call func() if you have some specific subtype. In this case, it means that you might want to say that func's parameter should be the narrowest possible type, which is the never type:
interface Foo {
func: (arg0: never) => number;
}
Now your subtype works with no error:
interface Bar extends Foo {
func: (arg0: SolidType) => number; // okay, no error
}
And the takeFoo() from above is no longer valid, so you don't have to worry about someone mistaking a Bar for something that accepts any possible arg0:
function takeFoo(foo: Foo) { foo.func("this is fine"); } // error
// ---------------------------------> ~~~~~~~~~~~~~~
If your use case requires some particular behavior with func() on the base type Foo, then you might have to tweak never to something else. In order to be type safe though, subtype function parameters can get wider but not narrower. If you find that you really need the opposite, then you might choose to give up type safety by using something like the any type instead of never. That could lead to takeFoo()-style runtime errors, so you'd need to be careful, which is always true when you intentionally loosen type safety.
Playground link to code
you can use Generic types here instead of unknown.
For example:
interface Foo<T> {
func: (arg0: T) => number;
}
type SolidType = {
someNumber: number;
}
interface Bar extends Foo<SolidType> {
func: (arg0: SolidType) => number;
}
const x: Bar = {
func: (arg0) => {
return arg0.someNumber;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With