Skip to content

keyof ignores constraint #44985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alesmenzel opened this issue Jul 12, 2021 Β· 6 comments
Closed

keyof ignores constraint #44985

alesmenzel opened this issue Jul 12, 2021 Β· 6 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@alesmenzel
Copy link

Bug Report

πŸ”Ž Search Terms

keyof, generic constraint

πŸ•— Version & Regression Information

  • This is a crash ❌ No
  • This changed between versions _______ and _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about keyof
  • I was unable to test this on prior versions because it seems to be consistent behavior in all tested versions

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

// Implementation of event emitter - sample without all methods
type Listener<Event> = (event: Event) => void;

// Here the "key: K" complains "An index signature parameter type must be either 'string' or 'number'"
type Listeners<Events, K extends string & keyof Events> = {[key: K]: Listener<Events[K]>[]}

class EventEmitter<Events extends {[key: string]: unknown}> {
  #listeners: Listeners<Events, keyof Events>

  constructor() {
    this.#listeners = {}
  }
}

// Usage
type MyEvents = {
    connect: void
    reconnect: number
    error: Error
    disconnect: void
}
const emitter = new EventEmitter<MyEvents>()

πŸ™ Actual behavior

I am getting ts errors even though the types work as expected. See // usage section

πŸ™‚ Expected behavior

The keyof returns the same type as the constraint generic and thus will be valid as index signature.

@MartinJohns
Copy link
Contributor

Having keyof T as an indexer type would require #26797.

@alesmenzel
Copy link
Author

So, does that mean it is currently not possible to do?
btw. why is K extends string & keyof Events not enough to make it an indexable type since it requires 'string' or 'number' and the generic K must be a string and also keyof Events?

@MartinJohns
Copy link
Contributor

The type string & keyof Events is the same as keyof Events. And a union of keys can not be an index signature. You can use a mapped type instead.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jul 12, 2021
@RyanCavanaugh
Copy link
Member

This works without issue

type Listeners<Events, K extends keyof Events> = Record<K, Listener<Events[K]>>;

@alesmenzel
Copy link
Author

alesmenzel commented Jul 12, 2021 β€’

Thanks guys,
the error message wasnt clear to me, but know I understand that you cant use a union of types which you get from the keyof - e.g. type union = 'a' | 'b' | 'c' to index but instead use it in either Record which handles the union case or use mapped type like {[key in K]: ... } instead of {[key: K]:

Lastly, do you know why there is the last error at the .push(listener) line in .on() method ts playground

Argument of type 'Listener<Events[Type]>' is not assignable to parameter of type 'Listener<Events[keyof Events]>'.
  Type 'Events[keyof Events]' is not assignable to type 'Events[Type]'.
    Type 'keyof Events' is not assignable to type 'Type'.
      'keyof Events' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.
        Type 'string | number | symbol' is not assignable to type 'Type'.
          'string | number | symbol' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.

            Type 'string' is not assignable to type 'Type'.
              'string' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint 'string | number | symbol'.(2345)

it seems to me that those types should be the same, no? One is Events[keyof Events] (so basically any values of the Events) and the second one is the concrete event Events[Type] where the type is defined as constraint of keyof Events

  on<Type extends keyof Events>(type: Type, listener: Listener<Events[Type]>): Unsubscribe  {
    if (!type) throw new Error('Cannot call <EventEmitter>.on without a type');
    if (!listener) throw new Error('Cannot call <EventEmitter>.on without a listener');
    this.#listeners[type] = this.#listeners[type] || [];
    this.#listeners[type].push(listener);
    return () => {
      this.off(type, listener);
    }
  }

@alesmenzel
Copy link
Author

Looks like I had a mistake in the mapped type, working version for anyone interested
ts playground

Thanks @MartinJohns and @RyanCavanaugh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

3 participants