【问题标题】:TypeScript Template Literals break generic constraintsTypeScript 模板文字打破了一般约束
【发布时间】:2021-12-25 16:08:40
【问题描述】:

我正在尝试编写一个支持事件和子事件的 Sub-Pub 类,由以下语法表示:

publisher.on("message:general", ... ) // subscribe to all messages
publisher.on("message", ... ) // subscribe to messages in general

为此,我使用 TypeScript 模板文字。

问题在于,虽然它可以广泛使用,但它似乎打破了一般约束。我做错了吗?

这是目前的样子:

TS Playground link for convenience

interface ChatEvents {
  connect: {
    user: string;
  };
  disconnect: {
    user: string;
    reason: "banned" | "timeout" | "leave";
  };
}

declare type ChatEvent = keyof ChatEvents;

interface SubEvents extends Record<ChatEvent, string> {
  connect: "general" | "watercooler" | "lobby";
  disconnect: "voice" | "text";
}

declare type EventWithSubEvent<T extends ChatEvent> = `${T}${
  | ""
  | `:${SubEvents[T]}`}`;

// "connect" | "connect:general" | "connect:watercooler" | "connect:lobby"
declare type ChatConnectEvent = EventWithSubEvent<"connect">;
// "disconnect" | "disconnect:voice" | "disconnect:text"
declare type ChatDisconnectEvent = EventWithSubEvent<"disconnect">;

// So far so good!

// Let's write some generics:
declare function subscribeToEvent<E extends ChatEvent>(
  event: E,
  callback: (payload: ChatEvents[E]) => void
): void;

subscribeToEvent("connect", (payload) => {
    // Correctly extracts matching type
    type TPayload = typeof payload; // { user: string; }
});

declare function subscribeToSubEvent<E extends ChatEvent>(
  event: EventWithSubEvent<E>,
  callback: (payload: ChatEvents[E]) => void
): void;

subscribeToSubEvent("connect:general", (payload) => {
    // Extracts all possible payload types instead of only the `connect` one
    /*
        {
            user: string;
        } | {
            user: string;
            reason: "banned" | "timeout" | "leave";
        }
    */
    type TPayload = typeof payload;
});

subscribeToSubEvent("connect:voice", () => {}) // Should fail but doesn't. `:voice` is a subevent of `disconnect`, not `connect`.

【问题讨论】:

  • 我认为问题可以简化到第 18 行,粘贴到下面看看结果类型:type X = EventWithSubEvent&lt;ChatEvent&gt; 这个类型太宽了

标签: javascript typescript generics


【解决方案1】:

我想我找到了解决办法。

问题是模板类型显然不是distribute over unions

要解决此问题,请查看此简化版 playground

诀窍是使用从任何扩展的泛型类型指定模板类型(只是为了强制联合分布)

declare type EventWithSubEvent<T extends ChatEvent> = T extends any ? `${T}:${SubEvents[T]}` : never;

这是your fixed playground

【讨论】:

  • 有趣的是,您的“固定游乐场”链接无法正确处理有效负载类型,而您的简化游乐场却可以。两者的唯一区别是:的可选性
  • EventWithSubEvent&lt;K&gt; | K 作为另一种使: 可选的方法似乎也不起作用。
  • 对不起,我不明白,有什么问题?
  • TPayload的值
  • @paroxyzm 的问题是 subscribeToSubEvent("disconnect:voice", (p) =&gt; p.reason) 不会推断它是断开连接事件,因此 p.reason 不被认为是安全的。
【解决方案2】:

一般来说,我发现从参数中推断出泛型类型所需的打字稿越少,泛型就越多(尤其是在提供体面的智能感知辅助时)。在这种情况下,如果您将泛型设置为作为第一个参数传递的确切文本(仅限于所有有效选项),则使用辅助类型仅提取 'connect' | 'disconnect' 部分以在回调中使用它会更顺畅。 (playground)

interface ChatEvents {
  connect: {
    user: string;
  };
  disconnect: {
    user: string;
    reason: "banned" | "timeout" | "leave";
  };
}
interface SubEvents extends Record<keyof ChatEvents, string> {
  connect: "general" | "watercooler" | "lobby";
  disconnect: "voice" | "text";
}
type AllChatEvents = {[K in keyof ChatEvents]: K | `${K}:${SubEvents[K]}`}[keyof ChatEvents]

// helper to extract the 'connect'|'disconnect' from an event
type ExtractEvt<T extends AllChatEvents> = T extends keyof ChatEvents ? T : T extends `${infer A}:${string}` ? A : never
declare function subscribeToSubEvent<E extends AllChatEvents>(
  event: E,
  callback: (payload: ChatEvents[ExtractEvt<E>]) => void
): void;

subscribeToSubEvent("disconnect:voice", (payload) => {
  console.log(payload.reason) // detects that 'disconnect' is the payload type
});

subscribeToSubEvent("connect:voice", () => {}) // fails properly 

作为奖励,因为泛型约束是所有要传递的有效字符串,与其他泛型设置相比,智能感知将提供非常有用的结果,在您已经输入之前它无法确定正确的行为是什么:

【讨论】:

    猜你喜欢
    • 2016-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多