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

时间:2019-05-03 15:48:27

标签: typescript types

我有一个用例,其中一个外部查询返回一个对象,该对象的属性与我的接口之一相同。如您在示例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)

2 个答案:

答案 0 :(得分:2)

首先,我将为MessageMailbox类型添加一些与众不同的属性。 TypeScript的类型系统是structural而不是标称系统,因此,如果MessageMailbox都具有相同的确切结构,则尽管它们的名称不同,编译器仍会认为它们是相同的类型。因此,让我们这样做可以避免潜在的问题:

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

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

并且因为类型系统不是名词性的,所以它实际上并不关心您为类型或接口提供的名称。因此,即使在编译时,编译器也无法为您提取接口的名称。

如果您正在寻找编译时解决方案,则将需要重构。类型名将被忽略,但对象的键名则不会被忽略(因为属性键在运行时存在,并且具有不同键的两种类型实际上是不同的类型)。因此,您可以改用类似AllContainer的类型:

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的属性相关的句柄。

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

答案 1 :(得分: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,但是当使用此方法,它变得非常容易,因为您不需要将任何类型传递给函数,而且可以返回正确的返回类型。