#How come these function types aren't equal?

21 messages · Page 1 of 1 (latest)

grim steeple
#

If I try to exactly recreate the type of another function using (...args: Parameters<OtherFn>) => void the types do not match. Why is this?

Playground: https://www.typescriptlang.org/play?#code/C4TwDgpgBAogjgVwIYBsA8ANANFAmgPigF4oAKNAFX1IEpjCKoIAPYCAOwBMBnKDKAPxQAjFABcUAEx0WbLr0rU6RBk1YceeQSPFTtwAE4JoEgGapuEANwAoUJCgBJbhSMRKauZsPHCJCrY2pgjsAMbAAJYA9uxQpsKKtFAA3jZQUPbQAGLsoiSkAHRFSAYA5twSFADaALrKhABuURGctumZUDmSxGRFBSXlEgAKJUgAthBsBtxoOcL49VBNLYHt4NAA+j3OrsZo8Mjoczhd+Pi2AL42QSHh0bGmkrO5nhq8hcVlFVBI7CC1i2WnCUKTSGXWnXY3XyfQG3xGBnGkwg02e80BzVa1zWDi2JB2bn2iFQaJOULOlyAA

I want this functionality in a type safe EventEmitter class:

type EventEmitterEvents = Record<string, (...args: any[]) => void>;
export class EventEmitter<Events extends EventEmitterEvents> {

/* ... */

    once<Event extends keyof Events>(event: Event, listener?: Events[Event]): Promise<Parameters<Events[Event]>> {
        return new Promise(resolve => {
            let self = this;
            function listenWrapper(this: any, ...args: Parameters<Events[Event]>) {
                if (listener)
                    listener.apply(this, args);
                
                self.off(event, listenWrapper);
                resolve(args);
            }
            this.on(event, listenWrapper);
        });
    }
```but `listenWrapper` does not match the type `Events[Event]`.
cunning thistleBOT
#

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

DayDun#2309

Preview:```ts
type Equal<X, Y> = (<T>() => T extends X
? 1
: 2) extends <T>() => T extends Y ? 1 : 2
? true
: false
type IsTrue<T extends true> = T

function f1<T>() {
type Fn1 = (...args: T[]) => void
type Fn2 = (...args: Parameters<Fn1>) => void

type _ = IsTrue<Equal<Fn1, Fn2>>
...```

lethal cape
#

Fn1 is a type parameter so it just has to be a subtype of that constraint. it could have properties defined on it besides its call signature, for example

grim steeple
lethal cape
#

listener?: Events[Event] could be any property of Events, which is itself a type parameter constrained to EventEmitterEvents, so yeah you don't confidently know very much about it at the implementation site. being generic like that is giving callers the freedom to do whatever they want, including wacky things like funky function types with properties

#

Is there any sane reason to support functions with additional properties here?
i haven't had coffee yet so am not very sharp at the moment, but i can't think of any. you should be able to structure your function to not care about the specific function type. what's the actual error you are seeing in your real code?

grim steeple
#

but shouldn't Events only consist of (...args: any[]) => voids?

lethal cape
#

maybe you can share a more complete example of your real thing?

grim steeple
#
type EventEmitterEvents = Record<string, (...args: any[]) => void>;
export class EventEmitter<Events extends EventEmitterEvents> {
    events: {[Event in keyof Events]?: Events[Event][]} = {};
    
    on<Event extends keyof Events>(event: Event, listener: Events[Event]) {
        if (!(event in this.events))
            this.events[event] = [];
        
        this.events[event]!.push(listener);
    }
    
    off<Event extends keyof Events>(event: Event, listener: Events[Event]) {
        if (!(event in this.events)) return;
        
        let idx = this.events[event]!.indexOf(listener);
        if (idx === -1) return;
        this.events[event]!.splice(idx, 1);
        
        if (this.events[event]!.length === 0)
            delete this.events[event];
    }
    
    once<Event extends keyof Events>(event: Event, listener?: Events[Event]): Promise<Parameters<Events[Event]>> {
        return new Promise(resolve => {
            let self = this;
            function listenWrapper(this: any, ...args: Parameters<Events[Event]>) {
                if (listener)
                    listener.apply(this, args);
                
                self.off(event, listenWrapper as any);
                resolve(args);
            }
            this.on(event, listenWrapper as any);
        });
    }
    
    emit<Event extends keyof Events>(event: Event, ...args: Parameters<Events[Event]>) {
        if (!(event in this.events)) return null;
        
        for (let listener of this.events[event]!) {
            listener.apply(this, args);
        }
    }
}
lethal cape
grim steeple
#

once is the relevant method, I've put as any to make it compile, but it doesn't feel like it should be necessary to bypass the type system here

lethal cape
#

actually only all of its string properties. you haven't placed any constraints on symbols

grim steeple
#

actual error is Argument of type '(this: any, ...args: Parameters<Events[Event]>) => void' is not assignable to parameter of type 'Events[Event]'.ts(2345)

#

how would I specify a more strict type here though?

lethal cape
#

seems like the user-supplied functions are also allowed to return something other than void (i guess void acts a bit like unknown for subtyping rules when it is in return position?), which is part of the problem

#

lemme play around a bit

#

this works, though it's not very elegant:

cunning thistleBOT
#
mkantor#7432

Preview:```ts
type EventEmitterEvents = Record<
PropertyKey,
(...args: any[]) => void

type VoidFunctionWithSameParameters<
T extends (...args: any[]) => void

= (...args: Parameters<T>) => void

export class EventEmitter<
Events extends
...```

lethal cape
#

there might be ways to re-parameterize the generic stuff to only refer to parameter list types since it seems like that's all you care about, but i gotta switch my focus to a work thing so don't have time right now to play around further (sorry)

grim steeple
#

hmm alright. thanks a lot anyways

vocal portal
#

!resolved