#Express return type using generic type variable

73 messages ยท Page 1 of 1 (latest)

earnest jasperBOT
#
m.a.t.t.t#0

Preview:ts // define AST node structure enum NodeKind { FooStmt = 'FooStmt', BarExpr = 'BarExpr', } type FooStmt = { kind: NodeKind.FooStmt expr: Expr } type BarExpr = { kind: NodeKind.BarExpr value: number } type Stmt = FooStmt type Expr = BarExpr export type Node = Stmt | Expr ...

iron owl
#

hello again, i had hoped to square away this AST visitor yesterday but I'm back at it again today.

the visit function accepts different types of visitors, and I'd like to change the return type based on which type of visitor was provided

im also wondering if perhaps I should be using signature overloading for this instead?

#

yeah function overloads seems to handle this case a lot better

im wondering what kind of limitation I was coming up against in the original playground link?

earnest jasperBOT
iron owl
#

this correlation problem is wild, i cant quite wrap my head around it

toxic jackal
#

I'm not seeing the unknowns that your comments refer to

#
function visit<FooStmt, number, number>(node: FooStmt, visitor: PartialVisitor<number, number>): unknown (+1 overload)
#

like is when hovering line 87

iron owl
#

first or second link?

#

thanks to chatgpt i also figured out how to solve this without using overloads, i forgot about using infer inside the return type expression

toxic jackal
#

second

iron owl
#

oh sorry in the second link I left behind some comments from the first link

#

i'll also create an update with how i ended up solving it using infer

toxic jackal
#

Ok

earnest jasperBOT
#
m.a.t.t.t#0

Preview:ts // define AST node structure enum NodeKind { FooStmt = 'FooStmt', BarExpr = 'BarExpr', } type FooStmt = { kind: NodeKind.FooStmt expr: Expr } type BarExpr = { kind: NodeKind.BarExpr value: number } type Stmt = FooStmt type Expr = BarExpr export type Node = Stmt | Expr ...

toxic jackal
#

Oki, I don't think I'm going to try to understand it ๐Ÿ™‚

#

If it works, then cool

iron owl
#

hehe, i basically just forgot about using infer

#

the only thing i still dont' understand is the difference between extends Generic<any> and exends Generic<unknown>

if i use unknown it no longer works, if I use any it works

#

on lines 37 and 47

toxic jackal
#

Well any is more permissive

#

I would think it would only matter if you were using one of the params as a callback argument

iron owl
#

yeah the second param is a map of callbacks

toxic jackal
#

which line breaks when you use unknown?

#

so I have something concrete to look at

iron owl
#

47, the type variables for the visit function

toxic jackal
#

But, I don't see an error

earnest jasperBOT
#
sandiford#0

Preview:```ts
...
//type VisitorConstraint = Visitor<any, any>
type VisitorConstraint = Visitor<unknown, unknown>

// return type of visit function
type R<
N extends Node,
V extends VisitorConstraint

= V extends ExhaustiveVisitor<infer S, infer E>
? Transform<N, S, E>
: V extends PartialVisitor<infer S, infer E>
? [S, E] extends [Stmt, Expr]
? Transform<N, S, E>
: unknown
: unknown

// the guts
export function visit<N extends Node, V extends VisitorConstraint>(
node: N,
visitor: V
): R<N, V> {
/// impl todo
...```

iron owl
#

hmm i see what you mean, its not breaking in playground... but it does break in my program, i'll copy over more from my source file

toxic jackal
#

try never instead of unknown, just to see what happens

#

if a type is used only in an argument position, then never will be the most broad

#

if the type is used only in ordinary positions, then unknown

#

and if its used in both positions then you can only use an exact type, or any

earnest jasperBOT
#
m.a.t.t.t#0

Preview:ts // define AST node structure enum NodeKind { FooStmt = 'FooStmt', BarExpr = 'BarExpr', } type FooStmt = { kind: NodeKind.FooStmt expr: Expr } type BarExpr = { kind: NodeKind.BarExpr value: number } type Stmt = FooStmt type Expr = BarExpr export type Node = Stmt | Expr ...

iron owl
#

line 70, if <any, any> is changed to <unknown, unknown> i get lots of errors

i'll try never

#

yeah both never and unknown break, only <any, any> works

toxic jackal
#

I guess because TN is an argument type, and it depends on S and E

#

!:variance

earnest jasperBOT
#
tjjfvi#0
`!t6:variance`:

Here's the example with Dog and Animal, explaining co/contra/in variance:

  • Covariance: () => Dog is assignable to () => Animal, because Dog is assignable to Animal; it "preserves the direction of the assignability"
  • Contravariance: (Animal) => void is assignable to (Dog) => void, because something that expects an Animal can also take a Dog; it "reverses the direction of the assignability"
  • Invariance: (Animal) => Animal is not assignable to (Dog) => Dog, because not all returned Animals are Dogs, and (Dog) => Dog is not assignable to (Animal) => Animal, because something expecting a Dog cannot take any other kind of Animal
iron owl
#

im just gonna close this file now that it works and hopefully never open it again haha

toxic jackal
#

So yeah, you have S and E in both the param and return position

#

Yeah you need any

#

But you should probably try to understand variance if you don't already, it's an important concept.

#

And it will come up again

iron owl
#

oh I see what you mean

toxic jackal
#

In that situation you can only have a definite type like Visitors<string, string>

#

or just use any

#

Visitors<any, any> is fine when it's just a constraint in a generic. But if you were to make a list of visitors typed as Visitors<any, any>[] then you'd have a big mess when you tried to use one.

#

const list = [Visitor<string, string>, Visitor<number, number>] as const satisfies Visitors<any, any>[]

iron owl
#

oh yeah the users of this API would always provide the two concrete values

one thing I found interesting was that I had to create ExhaustiveVisitor to complement PartialVisitor

I had thought I could have accepted PartialVisitor and then done some kind of check to see if all fields in the object were populated, but it doesn't work like that

so even if uses define const visit: Visitor = {} and then fill in all the fields, the return type will still be unknown unless they explicitly use const visit: ExhaustiveVisitor

toxic jackal
#

That sort of thing is fine, as satisfies is just acting as a constraint

iron owl
#

i still dont understand satisfies, i've read a flew blog posts but it hasn't clicked with me yet

toxic jackal
#

It just does a check as if you were assigning to a variable of that type

iron owl
#

does it also change the type of the variable you're assigning to?

toxic jackal
earnest jasperBOT
#
const a: string | number = 1 // allowed because 1 is within string | number
//    ^? - const a: string | number
// but now a is string | number

const b = 1 satisfies string | number // allowed because 1 is within string | number
//    ^? - const b: 1
#
const lists = [
  [1, 2, 3],
  ['foo', 'bar', 'baz']
] satisfies (any[])[]

lists
// ^? - const lists: (number[] | string[])[]

const lists2 = [
  [1, 2, 3],
  ['foo', 'bar', 'baz']
] as const satisfies (any[])[]

lists2
// ^? - const lists2: [[1, 2, 3], ["foo", "bar", "baz"]]
toxic jackal
#

This is a contrived example, because you could just use unknown[][], because when you get into these invariant generics, it's handy

iron owl
#

how do i get the bot to replace the code?

toxic jackal
#

```ts twoslash

```

earnest jasperBOT
#
const listsA = [
  [1, 2, 3],
  ['foo', 'bar', 'baz']
]

listsA
// ^? - const lists: (number[] | string[])[]

const listsB = [
  [1, 2, 3],
  ['foo', 'bar', 'baz']
] satisfies (any[])[]

listsB
// ^? - const lists: (number[] | string[])[]
iron owl
#

ah thankyou ๐Ÿ™‚ so even without the satisfies keyword, the types are the same?

toxic jackal
#

yeah* satisfies is only a check

earnest jasperBOT
#
const listsA = [
  [1, 2, 3],
  ['foo', 'bar', 'baz'],
  { a: 1, b: 2 }
]

const listsB = [
  [1, 2, 3],
  ['foo', 'bar', 'baz'],
  { a: 1, b: 2 }
//  ^
// Object literal may only specify known properties, and 'a' does not exist in type 'any[]'.
] satisfies (any[])[]
iron owl
#

ah okay so you define a broad check, but typescript will still type the variable using its inference?

toxic jackal
#

Yep

#

You can use X as const satisfies Y too

#

if you want as const inference

iron owl
#

ok, i think im understanding it a little more know, thank you - hopefully i can apply it soon

#

i still have a few remaining errors that i dont understand, i might just clobber them with never

earnest jasperBOT
#
m.a.t.t.t#0

Preview:```ts
enum NodeKind {
FooStmt = "FooStmt",
BarExpr = "BarExpr",
}
type FooStmt = {
kind: NodeKind.FooStmt
expr: Expr
}
type BarExpr = {
kind: NodeKind.BarExpr
value: number
}
type Stmt = FooStmt
type Expr = BarExpr
export type Node = Stmt | Expr

type Transform<V, S, E> = V ext
...```

iron owl
#

someone mentioned these are correlation problems, and that never is probably the best solution

anyway I need to actually try and implement now

  1. AsyncVisitor
  2. ExhaustiveAsyncVisitor

.. might need another coffee ..

#

also thank you for all your help these past few days, i wouldn't have been able to get this done without you!

if you're building anything where you need to pull web data, id be happy to set you up on my platform once it's live https://getlang.dev