繁体   English   中英

TypeScript 模板文字打破了通用约束

[英]TypeScript Template Literals break generic constraints

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

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

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

问题是,虽然它广泛有效,但它似乎打破了通用约束。 难道我做错了什么?

这是到目前为止的样子:

为方便起见,TS Playground 链接

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`.

我想我找到了解决方案。

问题是显然模板类型不会分布在联合上

要解决此问题,请查看这个简化的操场

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

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

这是你的固定游乐场

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

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 

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

将空引号之间的自动完成列表显示为 subscribeToSubEvent 函数的第一个参数,并建议所有有效选项

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM