我有一个用例,其中一个外部查询返回一个对象,该对象的属性与我的接口之一相同。如您在示例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)
答案 0 :(得分:2)
首先,我将为Message
和Mailbox
类型添加一些与众不同的属性。 TypeScript的类型系统是structural而不是标称系统,因此,如果Message
和Mailbox
都具有相同的确切结构,则尽管它们的名称不同,编译器仍会认为它们是相同的类型。因此,让我们这样做可以避免潜在的问题:
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
,但是当使用此方法,它变得非常容易,因为您不需要将任何类型传递给函数,而且可以返回正确的返回类型。