microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.34k stars 12.54k forks source link

Type information lost when passed as object key #42076

Open awerlogus opened 3 years ago

awerlogus commented 3 years ago

TypeScript Version: 4.2.0-dev.20201222

Search Terms: type information lost

Code

js:

/** @typedef {{ [K in string] : (data: any) => void }} Event */

/**
 * @template {Event} E
 */
function createEventEmitter () {
  /** @type {Set<{ [T in keyof E]: E[T] }>} */
  const events = new Set()

  /**
   * @template {keyof E} T
   *
   * @param {T} event event register to
   * @param {E[T]} listener listener to register
   */
  function on (event, listener) {
    events.add({ [event]: listener })
  }

  return { on }
}

module.exports = { createEventEmitter }

ts:

type Event = { [K in string] : (data: any) => void }

export function createEventEmitter<E extends Event> () {
  const events: Set<{ [T in keyof E]: E[T] }> = new Set()

  function on<T extends keyof E> (event: T, listener: E[T]) {
    events.add({ [event]: listener })
  }

  return { on }
}

Expected behavior: No errors

Actual behavior: In function on variable event has type T extends keyof E, but add operation shows an error

изображение

Related Issues: https://github.com/microsoft/TypeScript/issues/42060

amekse commented 3 years ago

Hi, you already have Event definition declared and createEventEmitter() is not receiving any parameters for which you need to set the type of the parameter. So you can directly use Event as your type definition, below is the probable solution:

type Event = { [K in string] : (data: any) => void }

export function createEventEmitter() {
  const events: Set<{ [T in keyof Event]: Event[T] }> = new Set()

  function on<T extends keyof Event> (event: T, listener: Event[T]) {
    events.add({ [event]: listener })
  }

  return { on }
}

I have naively tested the solution and it works, please test it for your case.

awerlogus commented 3 years ago

@sswtds

Your solution doesn't check event listeners' parameter types because there's no place where you can put event type declarations to emitter:

type Event = { [K in string] : (data: any) => void }

export function createEventEmitter() {
  const events: Set<{ [T in keyof Event]: Event[T] }> = new Set()

  function on<T extends keyof Event> (event: T, listener: Event[T]) {
    events.add({ [event]: listener })
  }

  return { on }
}

// SECTION Test

type RoomCreated = { roomCreated: (roomId: string) => void }

const emitter = createEventEmitter()

const logNumber = (x: number) => console.log(x)

// No error, but expected
emitter.on('roomCreated', logNumber)

The code above does the type check as expected:

type Event = { [K in string] : (data: any) => void }

export function createEventEmitter<E extends Event> () {
  const events: Set<{ [T in keyof E]: E[T] }> = new Set()

  function on<T extends keyof E> (event: T, listener: E[T]) {
    events.add({ [event]: listener })
  }

  return { on }
}

// SECTION Test

type RoomCreated = { roomCreated: (roomId: string) => void }

const emitter = createEventEmitter<RoomCreated>()

const logNumber = (x: number) => console.log(x)

// Error: Type 'string' is not assignable to type 'number'.
emitter.on('roomCreated', logNumber)

You can find full event emitter usage example here: xroom-app/events2

amekse commented 3 years ago

To check event listener parameter type you can do any one of the followings:

  1. Static checking by setting the Event data type to number: type Event = { [K in string] : (data: number) => void }
  2. Dynamic check by checking listener data type:
    function on<T extends keyof Event> (event: T, listener) {
      if(listener as number){
        events.add({ [event]: listener })
      }
    }

both solutions are based on top of my previous solution

punarinta commented 3 years ago

@sswtds your solutions are not unfortunately related to the aforementioned issue. Events in your case are pretty limited and cannot bear variable payloads depending on the event types. And as for dynamic checking, this breaks the whole advantage of a static typing there. :(