#Type-safe spread args

109 messages · Page 1 of 1 (latest)

velvet tartan
#

I have an interface, IHubServerResponse which is an interface of just functions. I won't post the full thing but I've added a screenshot with a sample. I have a waitFor function which will make a promise and wait for that signal to come in, and I have it setup so it requires an equally typed function relative to the signal it's waiting for. The problem is that the spread operator doesn't work (A spread argument must either have a tuple type or be passed to a rest parameter.), so it passes it in as a list. How do I do this?

    waitFor<K extends keyof IHubServerResponse>(type: K): Promise<Parameters<IHubServerResponse[K]>> {
        return new Promise((resolve) => {
            const callback = ((...args: Parameters<IHubServerResponse[K]>) => {
                this.unsubscribe(type, callback);
                resolve(args); // Spread doesn't work here 
            }) as IHubServerResponse[K];

            this.subscribe(type, callback);
        });
    }```
#

sendSubscription does it fine using this:


    private sendSubscription<K extends keyof IHubServerResponse>(type: K, ...args: Parameters<IHubServerResponse[K]>): void {
        for (const subscription of this.subscriptions[type]) {
            const typedSubscription = subscription as (...args: Parameters<ResponseCallback<K>>) => ReturnType<ResponseCallback<K>>;
            typedSubscription(...args);
        }
    }

Just not sure how to apply that here

limber gulch
#

So the issue is generic safety kicking in

#

you use an as cast in the second example

#

which makes it happy

#

but think about it shouldn't the second example, theoretically, work without a cast?

#

also you're not really typing the promise appropriately

#

you're saying it's a Promise<Parameters<IHubServerResponse[K]>>

#

that implies that it's a Promise<[arg0: T, arg1: U, ...]>

#

and so resolve expects [arg0: T, arg1: U, ...]

#

if you wrote resolve(1, 2, 3) you'd resolve the promise with the value of 1, not [1, 2, 3]

velvet tartan
limber gulch
#

right, it's generic safety being too conservative

#

so you'll need a strategic cast in both cases probably

#

there may be a workaround

#

though I'm pretty sure resolve(...args) doesn't make much sense

velvet tartan
limber gulch
#

new Promise((resolve) => resolve(1, 2, 3))

#

if you write this

#

the promise resolves to 1

#

resolve only takes 1 argument

#

the fact that you can't spread is annoying but in this case you don't want it anyways as far as I can tell

velvet tartan
#

Oh

#

I figured since promises resolved to functions that you'd be able to return multiple values to use with .then()

limber gulch
#

promises don't exactly resolve to functions

#

it's more like you can create a promise from a function using new Promise

#

the distinction is important because promises get certain powers regular functions wouldn't

#

approximately, the ability to resume later, this isn't 100% accurate but like if you write fetch(...) it doesn't have to finish immediately which can be quite uesful

limber gulch
#

but then still is only taking one arg

#

I just snuck in a destructuring of the tuple

limber gulch
# velvet tartan I figured since promises resolved to functions that you'd be able to return mult...

This may be a good read if you're unfamiliar with promises https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

MDN Web Docs

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises, this guide will explain consumption of returned promises before explaining how to create them.

#

the fact promises only resolve to one value is technically artifical but I think it makes sense

#

because otherwise const result = await new Promise((resolve) => resolve(1, 2, 3)) would do what exactly?

#

error because result is one value but the promise returns 3 values?

#

make result = [1, 2, 3]?

velvet tartan
limber gulch
#

yeah, no can do unfortunately

#

unless you write a wrapper around Promise I guess

#

but I'd recommend against that

velvet tartan
#

Ha! I'm not up for that lol

#

I am not smart enough for this

#

I needed some serious help from a friend just to get this far

velvet tartan
limber gulch
#

mmmm, there are definitely cases where this saves you from bugs

#

I think it's less a bug and more a deficit in TS's ability to reason that your program is sound

#

I would consider a bug something like erroneously allowing an unsound program to typecheck

velvet tartan
#

I see

#

Kind of a side quest, but I tried this to allow you to wait for multiple packets at once:

    waitFor<K extends keyof IHubServerResponse>(
        types: K | K[]
    ): Promise<Parameters<IHubServerResponse[K]>> {
        if (Array.isArray(types)) {
            return new Promise((resolve) => {
                const callback = ((...args: Parameters<IHubServerResponse[K]>) => {
                    types.forEach(type => this.unsubscribe(type, callback));
                    resolve(args);
                }) as IHubServerResponse[K];
    
                types.forEach(type => this.subscribe(type, callback));
            });
        }
    
        return new Promise((resolve) => {
            const callback = ((...args: Parameters<IHubServerResponse[K]>) => {
                this.unsubscribe(types, callback);
                resolve(args);
            }) as IHubServerResponse[K];
    
            this.subscribe(types, callback);
        });
    }
#

However, it's unclear which response is the one you got and I can't seem to figure out how to pass type to the callback

#

Is that even possible or is this just a pipe dream?

#

Actually I think my API for this is all wrong

#
connection.waitForGroup({
    "connectionSuccess": () => {},
    "connectionFailed": (reason: string) => { console.log("Connection failed: ", reason); }
});

This would probably be safer

#

I guess I need type safe Object.entries

#
for (const [type, callback] of Object.entries(types)) {

How can I indicate the type of type and callback?

#
    waitForGroup<K extends keyof IHubServerResponse>(types: Record<K, IHubServerResponse[K]>) {
        const typeMap = new Map<K, IHubServerResponse[K]>();

        for (const [type, callback] of Object.entries(types)) {
            const callbackWrapper = (...args: Parameters<IHubServerResponse[K]>) => {
                for (const [type, callback] of typeMap) {
                    this.unsubscribe(type, callback);
                }
                (callback as IHubServerResponse[K])(args); // HERE
            };

            this.subscribe(type as K, callbackWrapper as IHubServerResponse[K]);
            typeMap.set(type as K, callback as IHubServerResponse[K]);
        }
    }

This is giving me a fuss

limber gulch
#

Yeah so Object.entries is intentionally a bit conservative

#

because you can always assign excess keys to objects

velvet tartan
#

Oh wait I'm reusing callback twice which is no bueno

limber gulch
#
const coords = { x: 123, y: 456 }
const x: { x: number } = coords
#

what should Object.entries(x) be?

#

Array<["x", number]> doesn't account for the excess keys which could be anything

velvet tartan
#

Shouldn't x throw an error anyway?

limber gulch
#

nope

#

consider subclasses

#
class X {
    x: number = 123;
}

class Coords extends X {
    y: number = 456;
}

const x: X = new Coords();
#

these are secretly the exact same things

velvet tartan
#

Is there no generic form for entries?

#

oh gosh

limber gulch
#

I mean you want to be able to assign SomeSubclass to ParentClass right?

velvet tartan
#

Yea

limber gulch
#

but you're adding extra keys!

#

So Array<["x", number] | [string, unknown]> could be a sane type for Object.entries(x)

#

but TSC ends up making it just Array<[string, unknown]>

#

which is annoying but kinda makes sense

velvet tartan
#

Yea, but it is very annoying

#

Oh gosh I found more cursed TS stuff

limber gulch
#

This is a very common workaround:

type ExactEntries<O extends object> = Array<{
    [K in keyof O]: [K, O[K]]
}[keyof O]>

function exactEntries<O extends object>(o: O): ExactEntries<O> {
    return Object.entries(o) as ExactEntries<O>
}
velvet tartan
#

I get an error if I remove and then add a keyword

velvet tartan
limber gulch
#

Yeah and you have to know what you're doing as this can be unsound

#

but it's still a nice tool in your toolbox

velvet tartan
#
    waitForGroup<K extends keyof IHubServerResponse>(types: Record<K, IHubServerResponse[K]>) {
        const callbackMap = new Map<K, IHubServerResponse[K]>();

        for (const [type, callback] of Object.entries(types)) {
            const typedCallback = callback as (...args: Parameters<ResponseCallback<K>>) => ReturnType<ResponseCallback<K>>;
            const callbackWrapper = (...args: Parameters<IHubServerResponse[K]>) => {
                // If we get one, unsubscribe from all
                for (const [type, storedCallback] of callbackMap) {
                    this.unsubscribe(type, storedCallback);
                }
                typedCallback(...args);
            };

            this.subscribe(type as K, callbackWrapper as IHubServerResponse[K]);
            callbackMap.set(type as K, callback as IHubServerResponse[K]);
        }
    }
#

So I got this thing, right?

#

Start off with an error as expected

#

No errors here

#

Again, expected.

#

I press CTRL+Z and... ???

limber gulch
#

this does seem like a bug

#

can you playground this

#

like do the minimum amount of code that makes this reliable

velvet tartan
#

Uhhhh lemme see one sec

#

Oh gosh there's so many imports from generated code

#

I'm just gonna ship the generated code with if that's alright

#

Then use

connection = new ServerConnection("blah", "blah");
connection.waitForGroup({
    "connectionSuccess": 
});
#

In fact, if the function is made invalid for any reason, it fails to update unless if I delete it entirely or re-add the braces at the end

limber gulch
#

I'll see if I can mimize this into something the team can look into

#

This is probably a bit too large to look into productively

velvet tartan
#

To be honest, most of the generated folder can be ignored

#

And the min.ts file is just implementing the interface

limber gulch
#

I'm currently not able to reproduce

#

If I'm understanding you correctly the error should've been continuing to linger?

#

ideally you send me your whole project, package.json and all, as well as reproducible steps that are causing the lingering error

velvet tartan
#

Ummmm... project is kinda toast right now

#

Trying to recreate and it sucks

limber gulch
#

yeah problems like this tend to be like that