【问题标题】:Typescript generic interface where property name and type are "T"Typescript 通用接口,其中属性名称和类型为“T”
【发布时间】:2019-09-22 04:52:48
【问题描述】:

我有一个用例,其中外部查询返回一个对象,该对象具有与我的一个接口同名的属性。正如您在示例 executeQuery 函数中看到的那样,如果我将“消息”作为查询传递,那么我将返回一个具有 1 个名为“消息”的属性的对象。

我希望能够创建一个具有 1 个属性的 T 通用接口,其中名称是 T 的名称,类型是 T

我知道对此有运行时解决方案,但我想知道这是否可能在编译时仅使用 Typescript 类型。

共享代码:

function executeQuery<T>(query: "message" | "mailbox") {
    const data = query === "message" ?
        { Message: { id: 1 } } as unknown as T :
        { Mailbox: { id: 2 } } as unknown as T
    return { data: data }
}

interface Message {
    id: number
}

interface Mailbox {
    id: number
}

第一种解决方案:

interface AllContainer {
    Message: Message
    Mailbox: Mailbox
}

const messageQueryResult = executeQuery<AllContainer>("message")
console.log(messageQueryResult.data.Message.id)

const mailboxQueryResult = executeQuery<AllContainer>("mailbox")
console.log(mailboxQueryResult.data.Mailbox.id)

第二个解决方案:

interface MessageContainer {
    Message: Message
}

interface MailboxContainer {
    Mailbox: Mailbox
}

const messageQueryResult2 = executeQuery<MessageContainer>("message")
console.log(messageQueryResult2.data.Message.id)

const mailboxQueryResult2 = executeQuery<MailboxContainer>("mailbox")
console.log(mailboxQueryResult2.data.Mailbox.id)

我想做什么:

interface GenericContainer<T> {
    [T.Name]: T  // invalid Typescript
}

const messageQueryResult3 = executeQuery<GenericContainer<Message>>("message")
console.log(messageQueryResult3.data.Message.id)

const mailboxQueryResult3 = executeQuery<GenericContainer<Mailbox>>("mailbox")
console.log(mailboxQueryResult3.data.Mailbox.id)

【问题讨论】:

    标签: typescript types


    【解决方案1】:

    首先,我将为MessageMailbox 类型添加一些区别属性。 TypeScript 的类型系统是 structural 而不是名义上的,所以如果 MessageMailbox 具有相同的确切结构,编译器将认为它们是相同的类型,尽管它们具有不同的名称。所以让我们这样做以避免潜在的问题:

    interface Message {
        id: number,
        message: string; // adding distinct property
    }
    
    interface Mailbox {
        id: number,
        mailbox: string; // distrinct property
    }
    

    而且因为类型系统不是名义上的,它实际上并不关心你给类型或接口起的名字。因此,即使在编译时,编译器也无法提供任何句柄来提取接口的名称。

    如果您正在寻找编译时解决方案,您将不得不重构一些东西。类型名称被忽略,但对象的键名不会(因为属性键在运行时存在,并且具有不同键的两种类型实际上是不同的类型)。所以你也许可以从AllContainer-like 类型开始:

    interface AllContainer {
        Message: {
            id: number,
            message: string;
        }
        Mailbox: {
            id: number,
            mailbox: string;
        }
    }
    

    不要将类型称为Message,而是将其称为AllContainer["Message"]。您可以更进一步、更强烈地键入您的 executeQuery() 函数,为调用者提供更好的类型推断(同时在实现中仍需要类型断言):

    interface QueryMap {
        message: "Message",
        mailbox: "Mailbox"
    }
    
    
    function executeQuery<K extends keyof QueryMap>(query: K) {
        const data = (query === "message" ?
            { Message: { id: 1 } } :
            { Mailbox: { id: 2 } }) as any as Pick<AllContainer, QueryMap[K]>
        return { data: data }
    }
    
    
    const messageQueryResult = executeQuery("message")
    console.log(messageQueryResult.data.Message.id)
    
    const mailboxQueryResult = executeQuery("mailbox")
    console.log(mailboxQueryResult.data.Mailbox.id)
    

    全部编译...QueryMap 接口为编译器提供了executeQuery() 的参数如何与您要讨论的AllContainer 的属性相关的句柄。

    无论如何,希望这能给您一些关于如何进行的想法。祝你好运!

    【讨论】:

      【解决方案2】:

      解决此问题的方法之一是使用“函数重载”。

      你基本上做了 2 个签名,1 个用于“消息”响应,1 个用于“邮箱”响应:

      interface Message {
          id: number
      }
      
      interface Mailbox {
          id: number
      }
      
      interface Container<T> {
          data: T;
      }
      
      function executeQuery(name: 'message'): Container<{ Message: Message }>;
      function executeQuery(name: 'mailbox'): Container<{ Mailbox: Mailbox }>;
      function executeQuery(name: string): Container<any>; // Fallback string signature
      function executeQuery(name: string): Container<any> { // Implementation signature, not visible
          switch(name) {
              case 'message': {
                  const res: Container<{ Message: Message }> = {
                      data: {
                          Message: {
                              id: 1,
                          },
                      },
                  };
                  return res;
              }
              case 'mailbox': {
                  const res: Container<{ Mailbox: Mailbox }> = {
                      data: {
                          Mailbox: {
                              id: 1,
                          },
                      },
                  };
                  return res;
              }
              default:
                  throw new Error('Cannot execute query for: ' + name);
          }
      }
      
      const messageQueryResult3 = executeQuery("message")
      console.log(messageQueryResult3.data.Message.id)
      
      const mailboxQueryResult3 = executeQuery("mailbox")
      console.log(mailboxQueryResult3.data.Mailbox.id)
      

      在为外部无类型系统定义类型时最好使用此实现,因为在此系统内部很容易出错,因为在其返回类型中使用了any,但是当使用这个时,它变得非常容易,因为您不需要将任何类型传递给函数,并且您会得到正确的返回类型。

      【讨论】:

        猜你喜欢
        • 2019-02-13
        • 2020-02-28
        • 2020-01-07
        • 2018-07-26
        • 2023-04-08
        • 1970-01-01
        • 2020-09-14
        • 2019-11-26
        • 1970-01-01
        相关资源
        最近更新 更多