interface Fn {
(data: Data): void
}
interface Data<Value = any, Deps extends any[] = [...any]> {
value: Value
list: Deps
}
function fn(data: Data<string, [number]>): void {
console.log(data)
}
const foo: Fn[] = [fn]
console.log(foo)
When tested against the TypeScript Compiler this code throws an error because Type 'any[]' is not assignable to type '[number]'
.
My intuition is that the defined fn
is being assigned to the generic Data
value, but the error states that the assignment goes in the other direction. Is this a bug or limitation with the TypeScript compiler? Or how should I think about this to have a better intuition?
CodePudding user response:
Vastly simplified example of the same problem:
type FruitSmasher = (fruit: 'apple' | 'orange' | 'pear') => void
const appleSmasher: FruitSmasher = (apple: 'apple') => undefined
// Type '"apple" | "orange" | "pear"' is not assignable to type '"apple"'.
appleSmasher('orange') // this is bad.
appleSmasher
is not assignable to type FruitSmasher
because fruit smasher may smash any fruit, but appleSmasher
only knows how to smash apples. So you cannot treat appleSmasher
like a FruitSmasher
.
It's backwards to your intuition here because Fn
here needs to be callable with any[]
as the Deps
. That means [123]
, or ['asd', 123]
or []
all need to be allowed as the Deps
in order to be considered the same type as Fn
.
However, fn
actually requires Deps
be [number]
, which means that any[]
isn't going to going to work.
So according to the type of Fn
. This should be valid:
function fn(data: Data<string, [number]>): void {
console.log(data.list[0].toString()) // this will crash
}
const foo: Fn[] = [fn] // ignore this very valid type error for now
// Valid for type `Fn`, catastrophic for function `fn`.
foo[0]({ value: 123, list: [] })
However fn
's Deps
are now unsatisfied. data.list[0]
would return undefined
, then would crash on .toString()
.
So yes [number]
is assignable to any[]
. But when the argument to a function is declared as any[]
, then anything more specific than that will break that since that function can't be guaranteed the type that it requires.
CodePudding user response:
Here's what's going on. You declare your interface for the Data
type, saying that the second value of the type is an array that can have any type:
interface Data<Value = any, Deps extends any[] = [...any]> {
value: Value
list: Deps
}
Then you go to use that Data type but say that the second value will be an array of numbers:
function fn(data: Data<string, [number]>): void {
console.log(data)
}
So the type says the array can be anything, but the function declaration says the Data
type guarantees the array will only be numbers, because you are passing an object that conforms to the Data
interface into something that cannot accept some of the possible forms of that interface.
Remember that typescript really only functions in the IDE and compiler. It ends up being built into JavaScript, an interpreted language with no data types. This means that the function fn
will actually not throw an error if you try to pass it an object that has strings in the array once it gets compiled, so the IDE and compiler need to flag it and refuse to accept the structural issue.