我正在尝试编写一个class
,它将为在interface
中指定并作为generic
传递的每个key + value
提供类型安全。
我已经设法让一切工作,除了我无法确保key
属于interface
。换句话说:我可以将在提供给generic
的interface
中找不到的string
值传递给generic
。
我只是不知道如何调整我的代码,以便我可以保证key
存在于interface
中。
代码的超精简版本:
type Listener<V = unknown[]> = (...values: V[]) => void;
type EventMap = Record<string, Listener>;
type Library<T> = Partial<Record<keyof T, Set<T[keyof T]>>>;
class EventBlaster<T extends EventMap> {
lib: Library<T> = {};
register<K extends keyof T>(eventName: K, listener: T[K]) {}
trigger<K extends keyof T>(eventName: K, ...values: Parameters<T[K]>) {}
}
///
/// Usage
type CustomMap = EventMap & {
change(value: string): void;
};
const customEvents = new EventBlaster<CustomMap>();
// Working register / trigger
customEvents.register('change', (value) => {});
customEvents.trigger('change', 'hello');
// TypeScript correctly throwing errors on wrong listener arguments
customEvents.trigger('change', '1st string', '2nd string');
// TypeScript failing to throw errors on undeclared event names
customEvents.register('nope', () => {});
customEvents.trigger('nope');
在这个例子中,我已经让所有的listener > argument values
按预期工作了--这很好,我认为这是最困难的部分......
但不幸的是,您可以看到,我可以使用string
值调用register / trigger
,而这些值不是CustomMap
类型上的keys
。
如果我不用每次定义interface
时都使用extend EventMap
作为EventBlaster
的generic
参数,那就太好了...但我的假设是,这是获得我所追求的行为的必要条件。
在这个TypeScript Playground链接中可以找到一个稍微健壮一些的代码示例。
1条答案
按热度按时间hwazgwia1#
在编写的代码中,任何任意字符串都是
CustomMap
中的有效键。CustomMap
的类型是Record<string, Listener> & { change(value: string): void; }
,看起来你认为这个类型等价于{ change(value: string): void; }
,但事实并非如此,交集类型说,这个值必须同时符合A和B的类型,这意味着任何符合Record<string, Listener>
和{ change(value: string): void;}
的值,所以任何类型,只要有监听器作为值,字符串作为键,并且具有特定的签名,密钥更改将符合类型。结果是CustomMap
支持任意字符串键控的侦听器值属性,只要它也将charge
作为属性。总的来说,我看到很多人严重误解了
Record<string, X>
的含义。Record<string, X>
是任意字符串键到值的Map。使用它告诉编译器接受任何属性名。我也看到很多人似乎并不真正理解交集的作用。它们不是继承。特别是,你不需要使用交集来强制泛型类型中的“扩展”。Typescript是结构类型化的。所以类型A需要做的是“扩展”类型B,使其符合它。你永远不需要显式地将两者联系起来。
类型
{change(value: string): void;}
已经扩展了EventMap,因为它具有值为Listeners的所有字符串键。