简体   繁体   English

属性名称和类型为“ T”的Typescript通用接口

[英]Typescript generic interface where property name and type are “T”

I have a use-case where an external query returns an object with a property with the same name as one of my interfaces. 我有一个用例,其中外部查询返回一个对象,该对象的属性与我的接口之一相同。 As you can see in the sample executeQuery function, if I pass in "message" as a query, then I will be returned an object with 1 property named "Message". 如您在示例executeQuery函数中所看到的,如果我将“消息”作为查询传递,那么我将返回一个带有1个名为“消息”的属性的对象。

I want to be able to create a generic interface of T that has 1 property where the name is the name of T and the type is T . 我希望能够创建具有1个属性的T的通用接口,其中名称为T的名称,类型为T

I understand there are run-time solutions to this, but I was wondering if this was possible using only Typescript types at compile time. 我知道有针对此的运行时解决方案,但我想知道是否可以在编译时仅使用Typescript类型。

shared code: 共享代码:

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
}

1st solution: 第一种解决方案:

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)

2nd solution: 第二解决方案:

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)

What I want to be able to do: 我想要做的是:

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)

First of all, I'm going to add some distinguishing properties to the Message and Mailbox types. 首先,我将向“ Message和“ Mailbox类型添加一些与众不同的属性。 TypeScript's type system is structural and not nominal, so if both Message and Mailbox have the same exact structure, the compiler will consider them the same types, despite their having different names. TypeScript的类型系统是结构性的,而不是名义上的,因此,如果MessageMailbox具有相同的确切结构,尽管它们的名称不同,但编译器将认为它们是相同的类型。 So let's just do this to avoid potential issues: 因此,让我们这样做可以避免潜在的问题:

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

interface Mailbox {
    id: number,
    mailbox: string; // distrinct property
}

And because the type system isn't nominal, it really doesn't care about the names you give to types or interfaces. 而且由于类型系统不是名义上的,因此它实际上并不关心您为类型或接口提供的名称。 So there isn't any handle the compiler can give you to extract the name of an interface, even at compile time. 因此,即使在编译时,编译器也无法提供任何句柄来提取接口的名称。

If you're looking for compile time solutions, you're going to have to refactor things. 如果您正在寻找编译时解决方案,则将必须重构。 Type names are ignored, but key names of objects are not (since property keys exist at runtime and two types with different keys are really different types). 类型名将被忽略,但对象的键名则不会被忽略(因为属性键在运行时存在,并且具有不同键的两种类型实际上是不同的类型)。 So you can maybe start with an AllContainer -like type instead: 因此,您可以AllContainer的类型开始:

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

And instead of referring to the type as Message , you'd refer to it as AllContainer["Message"] . 而不是将类型称为Message ,而是将其称为AllContainer["Message"] You can go further and more strongly type your executeQuery() function, with better type inference for callers (while still needing a type assertion in the implementation): 您可以走得更远,更强大地键入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)

That all compiles... the QueryMap interface gives the compiler a handle on how the parameter to executeQuery() is related to the property of AllContainer you want to talk about. 全部编译QueryMap ... QueryMap接口为编译器提供了一个句柄,用于确定executeQuery()的参数如何与您要讨论的AllContainer的属性相关AllContainer

Anyway, hope that gives you some idea of how to proceed. 无论如何,希望能给您一些有关如何进行的想法。 Good luck! 祝好运!

One of the ways how you can solve this is by using "function overloads". 解决此问题的方法之一是使用“函数重载”。

You basically make 2 signatures, 1 for a "message" response, and 1 for a "mailbox" response: 您基本上进行了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)

This implementation is the best used when defining the types for an external type-less system, as it is quite easy to make a mistake inside this system because of the use of any in its return type, but when using this, it significantly becomes easier, as you don't need to pass any type to the function, and you get the correct return type back. 该实现是在为外部无类型系统定义类型时最好使用的方法,因为在系统内部由于使用返​​回类型中的any一个很容易犯错,但是使用此实现时,变得非常容易,因为您不需要将任何类型传递给该函数,并且可以返回正确的返回类型。

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

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