1

I am trying to spread function arguments in a position where varying arguments are accepted:

interface ClientEvents {
  ready: [client: string]
  interactionCreate: [interaction: number]
}

declare class Client {
  public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaited<void>): this;
  public on<S extends string | symbol>(
    event: Exclude<S, keyof ClientEvents>,
    listener: (...args: any[]) => Awaited<void>,
  ): this;

  public once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaited<void>): this;
  public once<S extends string | symbol>(
    event: Exclude<S, keyof ClientEvents>,
    listener: (...args: any[]) => Awaited<void>,
  ): this;
}

type MyEvent<T extends keyof ClientEvents> = {
  name: T,
  execute: (...args: [...args: ClientEvents[T], commands: string]) => Awaited<void>
}

declare const readyEvent: MyEvent<"ready">
declare const interactionCreateEvent: MyEvent<"interactionCreate">
declare const events: {
  ready: typeof readyEvent,
  interactionCreate: typeof interactionCreateEvent
}

declare const client: Client

Object.values(events).forEach((event) =>
  client[event.name === "ready" ? "once" : "on"](event.name, (...args) =>
    event.execute(...args, "test")
  )
)

This errors with:

(parameter) args: [client: string] | [interaction: number]
Argument of type 'string | number' is not assignable to parameter of type 'never'.
  Type 'string' is not assignable to type 'never'.

How would I solve this? ClientEvents and Client are library types in reality so I can only control the rest of the code

1 Answers1

0

The problem is in 'event.execute', because it is a union of two functions:

Object.values(events).forEach((event) =>
    client[event.name === "ready" ? "once" : "on"](event.name, (...args) => {
        type EventExecute =
            | ((client: string, commands: string) => Awaited<void>)
            | ((interaction: number, commands: string) => Awaited<void>)

        event.execute
    })
)

See EventExecute - it is a type of event.execute. Just hover the mouse on event.execute

Playground

Please see this famous answer

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

and docs

if any candidates were inferred from contra-variant positions, the type inferred for V is an intersection of those candidates. Otherwise, the type inferred for V is never.

and my article

and this answer with simplified example.

Further more, client[event.name === "ready" ? "once" : "on"] is a set of overloadings. In fact, args argument is a union of string and number. I would say that it is not the best way of using publish\subscribe pattern in typescript.

THe best way to handle this in typescript is to use ternar operator:


Object.values(events).forEach((event) => {
    return event.name === "ready"
        ? client.once(event.name, (args) => {
            event.execute(args, 'test')
        })
        : client.on(event.name, (args) => {
            event.execute(args, 'test')
        })

})

Playground

P.S. From my experience, TS does not play well with computed properties. It lways creates some issues. Maybe thsi is the reasom why a lod of languages with sound type system does not have computed properties at all.