我很抱歉这个可怜的标题,但我不知道如何描述我看到的问题.我的用例相当复杂,但我会尽我最大的努力将其提炼到最基本的部分.

我有一个配置对象的列表,其中有一个eventName,其值对应于可以在我的应用程序中触发的事件的名称.当所述事件被触发时,任何侦听器都会被调用一个适当类型的event对象.

例如:

type EventHandler<E> = (event: E) => void;
// Just a map of eventname -> handler (with appropriate event type as an arg)
type EventMap = {
  onStart: EventHandler<StartEvent>;
  onEnd: EventHandler<EndEvent>;
};
const configs = [
  buildConfig('onStart'),
  buildConfig('onEnd')
];

然后,我想映射到这个配置对象上,并构造另一个具有handlerFactory属性的(不同的)配置对象列表,该属性是为事件构建适当的事件处理程序的函数.这是一个函数的原因是提供一个闭包,该闭包可以为事件处理程序提供额外的上下文.

type AdditionalContext = unknown; // Actual type is not important for this example
type HandlerConfig<E extends keyof EventMap = keyof EventMap> = {
  eventName: E;
  handlerFactory: (context: AdditionalContext) => EventMap[E];
};

问题是,在我的.map回调中,TS无法推断正确的事件类型:

const handlerConfigs: HandlerConfig[] = configs.map(({ eventName }) => {
  return {
    eventName,
    handlerFactory: (context) => {
      // FIXME: Why is e `any`? It should be the event type that corresponds to the eventName event handler.
      return (e) => {
        console.log('Did something cool!', context);
      };
    }
  };
});

这里有一个TS playground显示错误.

我想做的事可能吗?

任何帮助将不胜感激!

更新:在上面的代码中添加了EventHandler的类型定义.

推荐答案

的主要问题是

const handlerConfigs: HandlerConfig[] = configs.map(({ eventName }) => {
    return {
        eventName,
        handlerFactory: (context) => {
            return (e) => { // error, implicit any
                console.log('Did something cool!', context);
            };
        }
    };
});

在回调函数中,eventName的类型是union.如果你可以narrow eventName到每个成员的unions 和判断它单独,你将有机会得到期望的行为:

configs.map(({ eventName }) => {
    if (eventName === "onEnd") {
        return {
            eventName,
            handlerFactory: (context) => {
                return (e) => {
                    console.log('Did something cool!', context);
                };
            }
        } satisfies HandlerConfig<"onEnd">;
    } else return {
        eventName,
        handlerFactory: (context) => {
            return (e) => {
                console.log('Did something cool!', context);
            };
        }
    } satisfies HandlerConfig<"onStart">;
});

但这是多余的.您可能认为编译器可以根据每个可能的缩小判断单个代码块,跟踪不同代码行中的联合成员之间的相关性,但这不是它的工作方式. (见https://twitter.com/SeaRyanC/status/1544414378383925250)

这是microsoft/TypeScript#30581的主题.

你能做的最简单的事情就是放弃编译器的精确判断,扩大内容或assert个内容,直到它停止抱怨.例如:

configs.map(({ eventName }) => {
    const ret: HandlerConfig = {
        eventName,
        handlerFactory: (context) => {
            return (e: StartEvent | EndEvent) => {
                console.log('Did something cool!', context);
            };
        }
    };
    return ret;
});

这是因为(x: StartEvent | EndEvent) => void满足两个事件处理程序.


另一方面,如果需要编译器遵循这样的逻辑,可以采用microsoft/TypeScript#47109中推荐的方法,在那里使用泛型而不是联合.

其思想是按照某种"基本"键-值映射类型编写操作,并将generic indexes into或基本类型写入mapped types,或在该类型上写入mapped types.

目标是希望通常看起来像联合的东西看起来像单个泛型类型. 下面是我在这个示例代码中的方法.基本类型如下:

interface EventMap {
    onStart: StartEvent,
    onEnd: EndEvent
}

(请注意,我呼叫了this EventMap,并将your EventMap重命名为HandlerMap,因为它更具描述性):

type HandlerMap<K extends keyof EventMap = keyof EventMap> =
    { [P in K]: (event: EventMap[P]) => void }

类型HandlerMap和以前一样(和你的EventMap一样),但现在它是泛型的,所以HandlerMap<K>只是带有键K的对象的一部分.

我们也要改变HandlerConfig:

type HandlerConfig<K extends keyof EventMap = keyof EventMap> = {
    eventName: K;
    handlerFactory: (context: AdditionalContext) => HandlerMap<K>[K];
};

HandlerMap<K>[K](event: EventMap[K]) => void相似,除了distributes over unions in 102. 也就是说,它是微软/TypeScript#47109中创造的distributive object type. 这假设如果K是一个并,那么handlerFactory也应该是一个并.如果你在乎这个,你可以写

type HandlerConfig<K extends keyof EventMap = keyof EventMap> = {
    eventName: K;
    handlerFactory: (context: AdditionalContext) => (event: EventMap[K]) => void;
};

但这最终只会模糊两个联盟成员之间的区别,并且您无法编写一个根据您获得的事件类型进行实际更改的版本.

不管怎样,从这里我们最终可以通过使map()回调泛型来写出handlerConfigs,说它将EventConfig<K>转换为HandlerConfig<K>:

const handlerConfigs: HandlerConfig[] = configs.map(
    <K extends keyof EventMap>({ eventName }: EventConfig<K>): HandlerConfig<K> => {
        return {
            eventName,
            handlerFactory: (context) => {
                return (e) => {
                    console.log('Did something cool!', context);
                };
            }
        };
    });

现在,当你判断e时,你会看到它是EventMap[K],你希望它是的类型(如果你愿意,你可以把它委托给依赖于eventName的东西).


这样的重构值得吗?从这个用例来看,显然不是这样.只要用一些宽泛的东西来注释e,然后继续前进是如此容易.但在某些情况下,这种重构是接近类型安全的唯一方法.

Playground link to code

Typescript相关问答推荐

Express Yup显示所有字段的所有错误

Angular NgModel不更新Typescript

如何使默认导出与CommonJS兼容

ApexCharts条形图未显示最小值.负值

使用KeyOf和Never的循环引用

如何 bootstrap TS编译器为类型化属性`T`推断正确的类型?

如何在ANGLE中注册自定义验证器

在保留类型的同时进行深层键名转换

使用TypeScrip的类型继承

下载量怎么会超过下载量?

TypeScrip-根据一个参数值验证另一个参数值

如何在Deno中获取Base64图像的宽/高?

如何以类型安全的方式指定键的常量列表,并从该列表派生所挑选的接口?

窄SomeType与SomeType[]

Typescript不允许在数组中使用联合类型

有没有一种方法可以防止我的组件包在其中安装样式组件'; node 模块中的s目录

必须从注入上下文调用Angular ResolveFn-inject()

如何向我的自定义样式组件声明 custom.d.ts ?

递归类型名称

从平面配置推断嵌套递归类型