#Inference fails when omitting spread parameter

22 messages · Page 1 of 1 (latest)

median raft
#

This is a strange issue, inference is failing, but only when I remove a useless ...args: evil parameter from a callback

playground

This works:

visit: (ref: ref, children: () => val[] | undefined | false, ...args: evil) => val,

but

visit: (ref: ref, children: () => val[] | undefined | false) => val,

does not.

Can anyone figure out how to keep inference without the spread parameter?

marble radishBOT
#

@median raft Here's a shortened URL of your playground link! You can remove the full link from your message.

webstrand#0

Preview:```ts
...
// This works fine, but mapSubtree has ...args: evil parameter
mapSubtree(node, visitor)
// ^?

// This should work fine, but inference fails for some reason. Note that mapSubtreeAlt lacks the ...args: evil parameter
mapSubtreeAlt(node, visitor)
// ^?

// Bypassing inference it works just fine
mapSubtreeAlt<T, ExpandableSubtree<T>>(node, visitor)
...```

noble folio
#

huh. interestingly a single optional trailing parameter doesn't trigger the different behavior

#

gonna play around with it a bit, will let you know what i learn

#

no big insights, but here's a slightly-more-minimal example:

marble radishBOT
#
mkantor#0

Preview:```ts
declare function mapSubtree<
val,
evil extends readonly unknown[]

(
visit: (child: val, ...args: evil) => unknown
): unknown

declare function mapSubtreeAlt<
val,
evil extends readonly unknown[]

(
visit: (
child: val,
...args: readonly unknown[]
) => unknown
...```

noble folio
#

it only seems to "work" when the rest parameter is annotated as a bare type parameter. for example this version also has the inference problem:

declare function mapSubtreeAltAlt<val, evil>(
  visit: (child: val, ...args: readonly evil[]) => unknown,
): unknown
median raft
#

thanks for the more minimal reproduction. Yeah it's strange even evil extends [] works, but the spread has to directly infer the generic

noble folio
#

i ended up making an even-more-minimal repro:

marble radishBOT
#
mkantor#0

Preview:```ts
declare function foo<A, Evil extends []>(
f: (a: A, ...evil: Evil) => unknown
): void
declare function bar<A>(f: (a: A) => unknown): void

declare function f<A extends {}>(a: A): void

foo(f) // works
//^?

bar(f) // doesn't work
//^?

bar(f<{}>) // works
//^?```

noble folio
#

i also briefly searched for open issues but couldn't find any. it was hard to know what to search for, though. it smells like a bug/oversight to me

fair holly
#

I think you actually want something more like this that doesn't collapse the generics?

marble radishBOT
#
sunshine_sower#0

Preview:```ts
...
declare function mapSubtreeAlt<
ref extends NodeLike<ref>,
val extends WeakKey

(
ref: ref,
visit: <T extends ref>(
ref: T,
children: () => val[] | undefined | false
) => val
): val
...```

fair holly
#

^^ This type-checks

median raft
# fair holly ^^ This type-checks

nope, it should be the same ref type throughout the traversal. The problem with that is that it resolves as ExpandableSubtree<NodeLike<any>>

#

if I actually implement that function I'll get one of the is compatible with the constraint, but ref may be instantiated with a different subtype of NodeLike<any>

fair holly
#

Personally, I'm kind of surprised either of the variants work because visitor is generic unlike most TypeScript values. My next recommendation would be to use an instantiation expression because it explicitly collapses the generic-ness of the function rather than forcing TypeScript to figure out how to collapse it (when IMO it shouldn't be collapsed implicitly; something something higher order functions).

marble radishBOT
#
sunshine_sower#0

Preview:ts ... mapSubtreeAlt(node, visitor<typeof node>) / ...

fair holly
#

I'm guessing the reason for the difference in behavior between the cases has to do with some arbitrary ordering by which tsc decides to resolve types. I ran into some weirdness with logically equivalent type constructs when when I was trying to do something like (can't exactly remember) infer a type from one parameter and then enforce it on another parameter and a return value. I ended up coming up with some hackaround solution involving splitting the type parameter into multiple type parameters with the latter type parameter(s) being type-constrained by the first one. This allowed me to force TypeScript's hand into resolving the type inference on the first parameter first before enforcing it on everything related to the latter type parameters.

#

Did you intentionally make visitor generic? The playground also type-checks if you remove the type parameter from visitor and let it use T from the enclosing function.

marble radishBOT
#
sunshine_sower#0

Preview:ts ... function visitor( ref: T, children: () => | ExpandableSubtree<T>[] | undefined | false ): ExpandableSubtree<T> { throw 1 } ...

median raft
#

ah true. Yeah it was intentionally generic, originally it was not a nested, but a top-level function