#Why are these two not equivalent?

64 messages · Page 1 of 1 (latest)

tawdry sundial
#

Minimal example of something I encountered in a different project. There was a different way to get the error in the first example to disappear, but it caused type names to look weird. I'd remembered that arrow functions and function declarations are not one in the same, so I'd attempted changing that at random to see if it works.

It did, giving me exactly the narrowing / inference behavior that I had wanted, but I'm not too sure why this works: my original impression is that the two are equivalent enough in a type declaration such that these two shouldn't be differ in their results.

narrow cometBOT
#
xemiru#0

Preview:```ts
class Hello<P> {
public myFn = <O, G>(input: {
a: (arg: P) => O
b: G
}) => input
}

const hello = new Hello<{}>()
hello.myFn({
a: arg => ({}),
b: {},
})

const makeHello = <P = {}>() => new Hello<P>()
type H = ReturnType<
ReturnType<typeof makeHello>["myFn"]

...```

#
xemiru#0

Preview:```ts
class Hello<P> {
public myFn = <O, G>(input: {a(arg: P): O; b: G}) =>
input
}

const hello = new Hello<{}>()
hello.myFn({
a: arg => ({}),
b: {},
})

const makeHello = <P = {}>() => new Hello<P>()
type H = ReturnType<
ReturnType<typeof makeHello>["myFn"]

...```

hushed knot
#

Methods are bivariant, so they are actually unsafe.

narrow cometBOT
#
nonspicyburrito#0

Preview:```ts
type Foo = {
doStuff(arg: unknown): void
}

const foo: Foo = {
doStuff(arg: string) {
arg.toUpperCase()
},
}

foo.doStuff(42)```

hushed knot
#

No compile error even though it explodes at runtime.

tawdry sundial
#

ah, so its still unsound and that second form just hides it?

#

what's the correct way to resolve it? i've tried the below, but while it gets it correctly the type hints show up somewhat awkwardly and i'm trying to avoid that

narrow cometBOT
#
xemiru#0

Preview:```ts
class Hello<const P extends any[]> {

public myFn = <O, G>(input: {
    a: <T extends P>(...arg: {
        [K in keyof T]: T[K]
    }) => O,
    b: G
}) => input

}

const makeHello = <P extends any[] = []>() => new Hello<P>()
type H = ReturnType<ReturnType<typeof makeHello>['myFn']>
...```

tawdry sundial
#

referring to this when i say it shows up awkwardly
with the previous methods, i would get a: number, but here i get a: { mapped type }[index]

hushed knot
#

@tawdry sundial What is the high level goal of the code? There are a few ways to fix your issue, but I don't understand what the purpose of this piece of seemingly convoluted code is.

#

Here is one way, by changing how myFn is generic over:

narrow cometBOT
#
nonspicyburrito#0

Preview:```ts
class Hello<P> {
public myFn = <
T extends {
a: (arg: P) => unknown
b: unknown
}

(
input: T
) => input
}

const hello = new Hello<{}>()
hello.myFn({
a: arg => ({}),
b: {},
})

const makeHello = <P = {}>() => new Hello<P>()
type H = ReturnType<
ReturnType<typeof makeHello>["myFn"]

...```

hushed knot
#

Here is another way, by properly specializing typeof makeHello:

narrow cometBOT
#
nonspicyburrito#0

Preview:```ts
class Hello<P> {
public myFn = <O, G>(input: {
a: (arg: P) => O
b: G
}) => input
}

const hello = new Hello<{}>()
hello.myFn({
a: arg => ({}),
b: {},
})

const makeHello = <P = {}>() => new Hello<P>()
type H = ReturnType<
ReturnType<typeof makeHello<{}>>["myFn"]

...```

hushed knot
#

But I feel like this might be a case of XY problem.

tawdry sundial
# hushed knot <@106088871602704384> What is the high level goal of the code? There are a few w...

goal is to create a function with constraints such that i can define an object

type Inputs = ReturnType<typeof inputs>;
const myInputs = inputs({
    params: [date(), number()],
    fn: (aDate, aNumber) => { /** something */ }
})

such that aDate and aNumber are properly inferred to be the types
denoted by properties of the objects inside params, i.e. aDate: DateValue,
aNumber: number and not unknown, { mapped type }[i], etc.

params is comprised of objects that denote how to construct a UI
component that can receive and thus provide a value of its type to eventually
give to fn, something like

// svelte
<script lang="ts">
    export let inputs: Inputs;
    let [ p0, p1 ] = inputs.params;
</script>

<div>
    <Param {param[0]} bind:this={p0} />
    <Param {param[1]} bind:this={p1} />
    <Button on:click={() => { inputs.fn(p0.get(), p1.get()) }}>Submit</Button>
</div>
tawdry sundial
# narrow comet

there're definitely other ways to approach this that i've also been investigating, though i did actually achieve my goals with this approach using the previously mentioned second approach

hushed knot
#

Your problem is the usage of type Inputs = ReturnType<typeof inputs>.

tawdry sundial
tawdry sundial
hushed knot
#

I mean, the Inputs will not have the specific information of what generic gets specialized.

hushed knot
tawdry sundial
#

oh for sure, but anything ingesting Inputs already does not care about the specific information
the goal was to make sure that the type information still exists while calling inputs

#

such that i can still receive intellisense when trying to define the body of fn

hushed knot
#

Can you show your actual code then? It seems to be different from code in OP

#

Well, if the two solutions I gave you don't satisfy.

tawdry sundial
#

code is on a work machine but let me see if i can package it up

tawdry sundial
#

points of interest are fn in filtering:56 / filters:18 and the surrounding constraint and fn2

hushed knot
#

Oof that's uh huge amount of code.

tawdry sundial
#

i did try to minify it in the first two examples because of that yeah \o/

#

main things are the same though, that generator function is trying to constrain the obj being passed into it
i cant get the little popup window to stay for a screenshot, but hovering over the parameters in filters

#

filters:17 constraint's a param is shown as Args[0] which i'd like to not have
a is still clearly the correct type since intellisense works and gives me stuff about DateValue when asking about it, just that type hint is weird

#

fn's date param is completely correct, though arbitrarily just changing it to not use arrow syntax in its definition in filtering:56 is what caused me to spawn the initial question in the first place since i couldnt figure out why that worked

tawdry sundial
# narrow comet

and uncommenting out fn2 in both files gives me a much more painful version of the error described in this first sample

#

though it still gets the function's parameter types correct, it just now fails compile

#

just noting, i dont mind not having a concrete answer to this as i'd technically already achieved what i wanted (and am refining a different version elsewhere), am just trying to understand why that one approach worked / why the other approaches had weird behaviors (tyvm for your time either way slpyCheer )

hushed knot
#

I mean, what about the two solutions I gave?

hushed knot
# narrow comet

In this solution specifically, is the type of H just a convenience so you don't have to manually write out the type again?

#

Because if that's the case, you can just specialize it into typeof makeHello<never>.

tawdry sundial
tawdry sundial
#

particularly lines 54/57/60 of that file all have this stupid looking Exclude<ReturnType<Inputs[InputType<Params[K]>]['fn']>, undefined> thing

hushed knot
#

Yeah, usually ReturnType<T>/Parameters<T> isn't something you want to use, it's more like a last resort because T's return types and parameters aren't exposed as standalone types

#

But that's mostly a clean code thing, you can do that refactor later on.

#

But it seems to me you can just do:

type Input = ReturnType<ReturnType<typeof withParams<never>>['input']>
//                                                  ^^^^^^^

To solve the issue, similar to the second solution I gave.

tawdry sundial
#

unsure if that changes things
constraint's param still shows up as Args[0] instead of DateValue,

#

and fn2's still broke in whatever way it is

#

fn remains unchanged, though
though it still has to not be in arrow syntax to achieve its correct state

hushed knot
#

Can you make a reproduction on TS playground?

#

I feel like the issue is not related to the method bivariant issue anymore, but something else in your code.

tawdry sundial
#

i can try to though i may just end up with those first two samples again since that was my original attempt at doing that
though just to clarify, the goal for me atm is to understand

  • why declaring fn with method syntax (dont know if thats the right way to refer to it) works
  • why declaring fn2 with its arrow syntax doesn't work
  • why declaring constraint with generics gives me Arg[0] instead of DateValue in type hints
#

trying to make sure i'm not wasting your time

#

since i think the first two points are answered by the method bivariant thing

hushed knot
#

Yep, first two is just because method syntax is bivariant (and so it's unsafe), so it's not that "it works with method syntax" but more so "it never worked, method syntax just hides the error from you"

#

Last point seems to be an unrelated issue that requires more code.

tawdry sundial
hushed knot
#

Yeah in general you want to do:

type Foo = ...
const makeFoo = (): Foo => { /* ... */ }

Rather than:

const makeFoo = () => { /* ... */ }
type Foo = ReturnType<typeof makeFoo>
#

But they are by all means equivalent so it's not the root cause of your 3rd issue.

tawdry sundial
#

i think i'm willing to mark this as resolved then either way
going about this in the way i think i need to probably needs a different approach and i dont intend to keep you here / this thread open for >3 days

tawdry sundial
#

tyvm for the effort!