簡體   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