【问题标题】:typescript losing precision when intersecting discriminated union与判别联合相交时打字稿失去精度
【发布时间】:2021-06-30 07:38:59
【问题描述】:

我有以下可区分的联合类型。

enum EventType {
  PostCreated = "POST_CREATED",
  UserCreated = "USER_CREATED",
}

type Event =
  | { type: EventType.PostCreated, data: { postId: number } }
  | { type: EventType.UserCreated, data: { userId: number } }

我想为使用type 参数检查data 参数的函数创建类型签名。

function createEvent<Type extends Event["type"]>(
  type: Type,
  data: (Event & { type: Type })["data"],
): Event {
  // ...
} 

但是,data 参数始终包含联合中的所有值(就好像我改写了 Event["data"] 一样)。最初的交叉点 (Event &amp; { type: Type }) 似乎有效,那么这里发生了什么?

其他上下文和测试用例在这里in the playground

【问题讨论】:

    标签: typescript


    【解决方案1】:

    你的类型type EventFromType&lt;Type extends Event["type"]&gt; = (Event &amp; { type: Type });最终表示为:

    type EventFromType<Type extends EventType> = {
        type: EventType.PostCreated & Type;
        data: {
            postId: number;
        };
    } | {
        type: EventType.UserCreated & Type;
        data: {
            userId: number;
        };
    }
    

    因此,当您将具体类型传递给 EventDataFromType 时,它会被评估为正确的结果。但是,当您通过 "data" 进行 generic 查找时,它不再依赖于 type 字段:

    type EventDataFromType<Type extends EventType> = ({
        type: EventType.PostCreated & Type;
        data: {
            postId: number;
        };
    } | {
        type: EventType.UserCreated & Type;
        data: {
            userId: number;
        };
    })["data"]
    
    // <=>
    
    type EventDataFromType<Type extends EventType> = {
        postId: number;
    } | {
        userId: number;
    }
    

    为了减轻这种行为,您应该对 EventFromType 进行惰性评估。仅在提供具体类型时才评估结果类型:

    type EventFromType<Type extends Event["type"]> = 
        Type extends unknown ? (Event & { type: Type }) : never;
    

    playground link

    然后一切都按预期进行。


    extends unknown 更新:

    要了解它为什么会这样工作,让我们引入Id&lt;T&gt; 类型来热切地评估T 类型的字段:

    type Id<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
    

    首先让我们看看原来的EventFromType类型里面发生了什么:

    type EventFromTypeEval<Type extends Event["type"]> = Id<Event & { type: Type }>
    
    type EventFromTypeEval<Type extends EventType> = {
        type: EventType.PostCreated & Type;
        data: {
            postId: number;
        };
    } | {
        type: EventType.UserCreated & Type;
        data: {
            userId: number;
        };
    }
    

    看起来该类型的结构中没有未知(双关语)类型,并立即得到热切评估。 EventType extends Event["type"] 拥有所有已知且数量非常有限的居民。这就解释了为什么通过data 字段进行通用 查找只会删除所有type 字段。

    为了让它懒惰地评估,我们必须注入一些不确定性。

    type EventFromTypeLazy<Type extends Event["type"]> = 
        Type extends unknown ? Event & { type: Type } : never
    

    T extends unknown 是这里的理想人选。虽然任何类型 T 都是 unknown 的子集,并且会落入条件的真实分支,但它也可能是 union 类型,并且打字稿必须将其处理为 distributive conditional type . Typescript 不能简单地丢弃extends unknown。它必须等待具体类型,然后才评估结果类型。让我们看看Id&lt;T&gt;的结果内部:

    type EventFromTypeLazyEval<Type extends Event["type"]> = 
        Id<Type extends unknown ? Event & { type: Type } : never>
    
    type EventFromTypeLazyEval<Type extends EventType> = (Type extends unknown ? Event & {
        type: Type;
    } : never) extends infer O ? { [K in keyof O]: O[K]; } : never
    

    playground link

    这里没有急切的评价。 Typescript 保持类型不透明,直到提供具体的 Type

    【讨论】:

    • 超级有趣——效果很好,但解决方案不直观。您是否有任何其他信息说明为什么 unknown 条件类型在这里有效?之前在TS中没有遇到过这种类型的问题。
    • 它只是创建了某种thunk 或期望参数的类型级函数。与它具有所有可用类型并且可以像实际上一样评估和优化它们的情况相反。
    • 抱歉,不太关注。我认识到EventFromType 是一个类型级函数,需要一个参数(Type),但Type extends unknown 是我发现令人困惑的条件。我猜这与惰性评估有关,但Type extends unknown 似乎不是一个直观的检查。 unknown 不是几乎所有其他类型的超类型吗?
    • 更新了答案。
    【解决方案2】:

    这是一种解决方案 (TS Playground)。

    enum EventType {
      PostCreated = "POST_CREATED",
      UserCreated = "USER_CREATED",
    };
    
    type PostCreatedEvent = { type: EventType.PostCreated, data: { postId: number } };
    type UserCreatedEvent = { type: EventType.UserCreated, data: { userId: number } };
    
    type EventMap = {
      [EventType.PostCreated]: PostCreatedEvent,
      [EventType.UserCreated]: UserCreatedEvent,
    };
    
    declare function createEvent<T extends EventType, E extends EventMap[T]>(
        type: T,
        data: E['data'],
    ): E;
    
    createEvent(EventType.PostCreated, { postId: 2 });
    createEvent(EventType.UserCreated, { userId: 2 });
    
    // @ts-expect-error
    createEvent(EventType.PostCreated, { userId: 2 });
    // @ts-expect-error
    createEvent(EventType.UserCreated, { postId: 2 });
    

    【讨论】:

    • 谢谢。 EventType 枚举实际上要大得多,因此手动编写所有条件分支并不是一个吸引人的选择。我已经有了一个可行的变体,但我更感兴趣的是为什么上述方法不起作用。
    • 当然——我自己不是三元组的粉丝。已更新为使用地图 ?
    • 最初的尝试没有奏效,因为createEvent 实现中使用的泛型很奇怪(它们并没有完全对齐)。请注意上述解决方案中E extends EventType, T extends EventMap[E] 的使用。
    • 最初尝试中的泛型有何奇怪之处?我的游乐场链接已经有一个使用地图解决问题的解决方案,但我仍然不明白为什么联合方法不起作用。
    猜你喜欢
    • 2020-03-16
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    • 1970-01-01
    • 2021-11-26
    • 2022-01-13
    • 2020-08-05
    • 1970-01-01
    相关资源
    最近更新 更多