1

Where I declare the array I am not sure what the type will be so I kept a generic key, however that results in the error

Type '(...args: ClientEvents[T]) => Promise<unknown>' is not assignable to type '(...args: [command: ApplicationCommand<{}>] | [command: ApplicationCommand<{}>] | [oldCommand: ApplicationCommand<{}> | null, newCommand: ApplicationCommand<{}>] | ... 63 more ... | [oldSticker: ...]) => Promise<...>'.
  Types of parameters 'args' and 'args' are incompatible.
    Type '[command: ApplicationCommand<{}>] | [command: ApplicationCommand<{}>] | [oldCommand: ApplicationCommand<{}> | null, newCommand: ApplicationCommand<{}>] | ... 63 more ... | [oldSticker: ...]' is not assignable to type 'ClientEvents[T]'.
      Type '[command: ApplicationCommand<{}>]' is not assignable to type 'ClientEvents[T]'.
        Type '[command: ApplicationCommand<{}>]' is not assignable to type 'never'.
          The intersection '[command: ApplicationCommand<{}>] & [command: ApplicationCommand<{}>] & [oldCommand: ApplicationCommand<{}> | null, newCommand: ApplicationCommand<{}>] & ... 63 more ... & [oldSticker: ...]' was reduced to 'never' because property 'length' has conflicting types in some constituents.ts(2322)
handlers.ts(6, 3): The expected type comes from property 'execute' which is declared here on type 'Event<keyof ClientEvents>'
import type { ClientEvents } from "discord.js"
import { logger } from "./logger"

type Event<T extends keyof ClientEvents> = {
  name: T
  execute: (...args: ClientEvents[T]) => Promise<unknown>
}

export const events: Array<Event<keyof ClientEvents>> = []

export function registerEvent<T extends keyof ClientEvents>(
  name: T,
  execute: (...args: ClientEvents[T]) => Promise<unknown>
) {
  events.push({ name, execute }) // errors at execute
}

Link to Playground

  • please share reproducable example – captain-yossarian from Ukraine Aug 07 '21 at 13:34
  • 1
    I added a playground link –  Aug 07 '21 at 13:41
  • Looks like another one for the [microsoft/TypeScript#30581](https://github.com/microsoft/TypeScript/issues/30581) pile. – jcalz Aug 07 '21 at 14:50
  • Thanks for linking this issue, is there any workaround until then? –  Aug 07 '21 at 15:01
  • I think before we can fix `registerEvent` you need to fix `Event`. The type `Event` is a single type where both `name` and `execute` are the full unions of their possible types, and any correlation is lost. A union-of-functions can be called with an intersection-of-parameters, and thus you get behavior I can't imagine you want, like [this](https://tsplay.dev/Na0r6w). Really you are looking for something like [existential types](https://en.wikipedia.org/wiki/Type_system#Existential_types), or a union like [this](https://tsplay.dev/WzoPEw). Which leads to ms/TS#30581. – jcalz Aug 07 '21 at 15:05

1 Answers1

0

MOre information about same problem you can find here

For the sake of brevity we can reduce your example to this:

interface ClientEvents {
  foo: [1],
  bar: [2, 2]
}

type Event<T extends keyof ClientEvents> = {
  name: T
  execute: (...args: ClientEvents[T]) => Promise<unknown>
}

export const events: Array<Event<keyof ClientEvents>> = []

export function registerEvent<T extends keyof ClientEvents>(
  name: T,
  execute: (...args: ClientEvents[T]) => Promise<unknown>
) {
  events.push({ name, execute }) // errors at execute
}

Since you want to infer T, I assume this is a valid call:

const result = registerEvent(
  'foo', (...args: [1]) => null as any
) // should compile


const result2 = registerEvent(
  'foo', (...args: [2,2]) => null as any
) // should fail

It is pretty straitforward. But wait, since T extends keyof ClientEvents I can pass a union as a type for ...args:

const result2 = registerEvent(
  'foo', (...args: [2,2] | [1]) => null as any
) // still compiles

Let's slightly change the function implementation. Le'ts just return an event.

export function registerEvent<T extends keyof ClientEvents>(
  name: T,
  execute: (...args: ClientEvents[T]) => Promise<unknown>
): Event<keyof ClientEvents> {
  return { name, execute } // <--- error
}

You will get this error:

The intersection '1 & [2, 2]' was reduced to 'never' because property '0' has conflicting types in some constituents

WHy this erorr occured?

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

Hence [1] & [2, 2] === never. execute is in contra-variant position with ClientEvents[T]. This behavior is by design and makes your code much safer.

Consider this example:

interface ClientEvents {
  foo: [1],
  bar: [2, 2]
}

type Event<T extends keyof ClientEvents> = {
  name: T
  execute: (...args: ClientEvents[T]) => Promise<unknown>
}

export const events: Array<Event<keyof ClientEvents>> = []

export function registerEvent<T extends keyof ClientEvents>(
  name: T,
  execute: (...args: ClientEvents[T]) => Promise<unknown>
): Event<keyof ClientEvents> {
  return { name, execute } // <------- error here
}


let invalid = (...args: [1] | [2, 2]) => Promise.resolve(args[0])

const result2 = registerEvent(
  'foo', invalid
) // still compiles

// can lead to runtime error, since "foo" expects 1
result2.execute(2, 2)

TS still is trying to tell you that something might go wrong and you should not do it.

Such behavior was not always. Previously, all function was bivariant to their arguments. Try to turn off strictFunctionTypes flag. You will see that your code will compile.

Let's try to go another way. Maybe we should to declare a type of all possible allowed events?

interface ClientEvents {
  foo: [1],
  bar: [2, 2]
}

type Event<T extends keyof ClientEvents> = {
  name: T
  execute: (...args: ClientEvents[T]) => Promise<unknown>
}

type Values<T> = T[keyof T]

type ValidEvents = Values<{
  [Prop in keyof ClientEvents]: Event<Prop>
}>

export const events: Array<ValidEvents> = []

export function registerEvent(event: ValidEvents) {
  events.push(event)
  return event
}

let invalid = (...args: [1] | [2, 2]) => Promise.resolve(args[0])

const result2 = registerEvent(
  { name: 'foo', execute: invalid }
) // still compiles

// but now you are not able to call execute with invalid arguments
result2.execute(2, 2) // 

Above approach requires a bit of refactor but is also a bit safer

UPDATE

If for some reason you don't want to refactor your function, you may overload it:

interface ClientEvents {
  foo: [1],
  bar: [2, 2]
}

type Event<T extends keyof ClientEvents> = {
  name: T
  execute: (...args: ClientEvents[T]) => Promise<unknown>
}

type Values<T> = T[keyof T]

type ValidEvent = Values<{
  [Prop in keyof ClientEvents]: Event<Prop>
}>

export const events: Array<ValidEvent> = []

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type Overloading =
  UnionToIntersection<Values<{
    [Prop in keyof ClientEvents]: (...args: [

      name: Prop,
      execute: (...args: ClientEvents[Prop]) => Promise<unknown>
    ]) => void
  }>>

const registerEvent: Overloading = (
  name: keyof ClientEvents,
  execute: (...args: any[]) => Promise<unknown>
) => {
  events.push({ name, execute }) // errors at execute
}

let invalid = (...args: string[]) => Promise.resolve(args[0])
let valid = (...args: [1]) => Promise.resolve(args[0])

const result2 = registerEvent(
  'foo', invalid
) // error, despite the fact that we have an any[]

const result3 = registerEvent(
  'foo', valid
) // ok

Be aware the intersection of function types produces overloadings.

however when using ClientEvents from the library, execute still produces an

My bad, I've been working with mocked data. TS can't bind together name and execute. Seems ClientEvents is a much complicated datastructure than I thought. Hence, you better stick with first solution, where name and execute are properties of same object.

export function registerEvent(event: ValidEvents) {
  events.push(event)
  return event
}

  • Thank you for the explanation, however when using `ClientEvents` from the library, `execute` still produces an [error](https://tsplay.dev/mx5aXW) –  Aug 07 '21 at 16:29