I have the following typing :
declare function linkedSignal<S, D>(options: {
source: () => S;
computation: (source: NoInfer<S>, previous?: {source: NoInfer<S>; value: NoInfer<D>}) => D;
}): D;
When using it this way :
linkedSignal({
source: () => 3,
computation: (source, previous) => {
return 3;
},
})
D
is infered as unknown
when previous
(the 2nd parameter) is defined, even though the computation
function returns a deterministic type.
Can you explain why this is happening and how we could improve the typing so the generic is correctly inferred.
Playground
I have the following typing :
declare function linkedSignal<S, D>(options: {
source: () => S;
computation: (source: NoInfer<S>, previous?: {source: NoInfer<S>; value: NoInfer<D>}) => D;
}): D;
When using it this way :
linkedSignal({
source: () => 3,
computation: (source, previous) => {
return 3;
},
})
D
is infered as unknown
when previous
(the 2nd parameter) is defined, even though the computation
function returns a deterministic type.
Can you explain why this is happening and how we could improve the typing so the generic is correctly inferred.
Playground
Share Improve this question edited Nov 19, 2024 at 22:28 Matthieu Riegler asked Nov 19, 2024 at 22:22 Matthieu RieglerMatthieu Riegler 56.1k27 gold badges146 silver badges198 bronze badges 2- It's essentially ms/TS#47599; when you have contextual typing and generics that depend on each other, TS has a hard time inferring. You could try to change to something like this version which should at least allow the return type to be inferred, but that contextual type still won't work, because of the dependency. If this matters you're probably just going to have to annotate things even if you "shouldn't have to". Does that fully address the q? If so I'll write an a or find a duplicate; if not, what's missing? – jcalz Commented Nov 19, 2024 at 23:32
- Yeah that's probably it, we're hitting a TS limitation. – Matthieu Riegler Commented Nov 19, 2024 at 23:58
2 Answers
Reset to default 0NoInfer
doesn't stop TS from infering, it just infers the generic as unknown
which is happening in your example. Since the most reliable source of guessing the type is at previous.value
it is what gets recognized as your D
.
It's obviously a bit out of context, so it's hard to say if it may help, but in this piece of code the type of value
doesn't seem that important. You are interested in what's returned. Removing the generic D
from that definition makes TS infer the return type as you wish it did.
In general, for a function of the form
(source: NoInfer<S>, previous?: {source: NoInfer<S>; value: NoInfer<D>}) => D
the return type depends on the input type, because both involve the generic type parameter D
. Telling TypeScript not to infer D
from the type of value
doesn't change this. If you assign a function like (source, previous) => ⋯
where you don't annotate the parameters but rely on contextual typing, TypeScript will decide it needs to defer evaluation of the function type until it knows D
, at which point it's too late. Syntactically at least, D
depends on the type of previous
, which depends on D
. That's circular and TypeScript gives up.
Now, it may well be that the function body's return value doesn't actually depend on previous
, in which case it's theoretically possible for TypeScript to inspect the function body and determine the return type without needing to know the type of previous
. You are imagining that the circularity can be broken inside the implementation of the callback function.
But this does not happen. It's effectively a design limitation of TypeScript. See microsoft/TypeScript#47599 for the wider issue where TypeScript could theoretically improve inference of generic type arguments in the presence of context-sensitive functions. There has been some progress here, as more scenarios are supported, but the fundamental limitation will always remain in some form. TypeScript's inference algorithm does not perform so-called "full unification", as discussed in microsoft/TypeScript#30134, and this is unlikely to change.
That means you either need to work around the problem or give up. For example, if your function actually doesn't depend on previous
and you're not actually using previous
, then maybe don't make it a parameter?
const foo = linkedSignal({
// ^? const foo: number
source: () => 3,
computation: (source) => {
return 3;
},
})
Or you can include it and annotate its type to the extent you care about it:
const foo= linkedSignal({
// ^? const foo: number
source: () => 3,
computation: (source, previous?: { source: number }) => {
return 3;
},
})
Or you could try to change your call signatures so the circularity is moved over so that the failure doesn't prevent D
from being inferred, such as adding a new type parameter constrained to D
:
declare function linkedSignal<S, D, E extends D = D>(options: {
source: () => S;
computation: (source: NoInfer<S>, previous?: { source: NoInfer<S>; value: E }) => D;
}): D;
const foo = linkedSignal({
// ^? const foo: number;
source: () => 3,
computation: (source, previous) => {
// ---------------> ^? {source: number, value: unknown}