首页 > 解决方案 > 泛型函数中的打字稿范围不能按预期缩小

问题描述

泛型函数中的打字稿范围不能按预期缩小

我有一个EventType枚举,EventType以及与其对应的参数之间的关系。

enum EventType { A, B };

interface EventParams {
    [EventType.A]: { name: string, age: number }
    [EventType.B]: { address: string }
}

EventType但是当我在泛型函数中使用这些定义并通过if范围阐明的值时,param并没有缩小到相应的类型EventParams

const run1 = function <T extends EventType>(eventName: T, param: EventParams[T]) {
    if (eventName === EventType.A) {
        // ts error here
        param.name
    }
}

它仍然看起来像是 中所有值的集合EventParams

这是另一个相关的问题:当我为每个创建处理回调时EventType

type EventCallbacks = {
    [EventName in EventType]: (param: EventParams[EventName]) => any
}

const callbacks: EventCallbacks = {
    [EventType.A]: param => {},
    [EventType.B]: param => {}
}

而在泛型函数中使用这些, 的类型callback依然没有缩小到对应的T泛型:

const run2 = function <T extends EventType>(eventName: T, param: EventParams[T]) {
    const callback = callbacks[eventName];
    // ts error here
    callback(param);
}

这是 Playground 中的完整示例

那么我的代码是否有问题或打字稿无法处理这种类型的缩小?

标签: typescripttypescript-generics

解决方案


想象一下这个场景

run1<EventType.A | EventType.B>(EventType.A, {address: 'foo'})

这是完全有效的代码。如果Textends EventType,这并不意味着它T是 of 之一EventType,它完全可以是一个 union。在这种情况下,您可以传递EventType.Bto和toeventName的对象,这会在运行时导致问题。所以你不能这样做。一种选择是使用类型保护,尽管我认为使用这样的有区别的联合会更好:EventType.Aparam

const run1 = function<T extends EventType>(
    data: T extends any ? {eventName: T, param: EventParams[T]} : never
) {
    if (data.eventName === EventType.A) {
        // no ts error here
        data.param.name
    }
}

现在 if Tis EventType.A | EventType.B,data将是类型

| {eventName: EventType.A, param: EventParams[EventType.A]} 
| {eventName: EventType.B, param: EventParams[EventType.B]}

打字稿能够推断出一切。确保不要破坏参数(不要这样做const {eventName, param} = data),因为在进行解构时,可区分联合中的连接会丢失。通常你可以if在类型缩小之后解构语句中的变量,但在这种情况下 ts 会有点混乱并且不能按预期工作,可能是因为泛型。

您还需要这个T extends any东西,以便如果T是联合,则为联合的每个成员创建一个单独的对象。如果你忽略它,你只会得到{eventName: EventType, param: EventParams[EventType]}什么不好。

实际上,这样一来,您实际上并不需要泛型,您可以像这样为事件/参数对的所有组合创建一个可区分的联合

const run2 = function(
    data: {
        [key in EventType]: {eventName: key, param: EventParams[key]}
    }[EventType]
) {
    if (data.eventName === EventType.A) {
        // Now typescript doesn't get confused when destructuring
        const {eventName, param} = data
        param.name
    }
}

回调也一样,如果T是联合,打字稿无法推断出正确的类型。这也可以使用有区别的联合来解决,请参阅Sandbox。但是,如果您只想const callback = callbacks[eventName]不检查eventName类型,它会有点复杂,您可能需要一些类型转换。


推荐阅读