[英]How to define a conditional return type based on if an object property is set in Typescript
interface Options {
flagA?: boolean
flagB?: boolean
}
function abc(name, options?: Options) {}
if(options.flagA) return some type
if(options.flagB) return some some other type
我想根据是否设置了 flagA 或 flagB 有不同的返回类型。 这可能吗?
我已经做到了类似的事情
function abc<T>(name, options?: T): T extends Options ? string : number { return }
const data = abc('test', { flagA: true });
根据 T 是否实现选项返回字符串或数字,但我不确定如何更具体地检查 options.flagA 是否具有值。
具体而言,我将定义这些接口:
interface FlagARet {
a: string;
}
interface FlagBRet {
b: string;
}
interface NoFlagRet {
x: string;
}
意图是如果options
将flagA
设置为true
,则abc()
将返回一个FlagARet
值; 否则,如果options
将flagB
设置为true
,则abc()
将返回一个FlagBRet
值; 否则abc()
将返回NoFlagRet
值。
这种类型的杂耍不是编译器会自动为你做的,甚至无法在编译时进行验证; 您需要向编译器准确说明您想要评估的 function 类型,并执行与类型断言等效的操作来告诉编译器您保证 function 实际上返回您声明的类型. 这很难真正保证。 可能存在边缘情况,因此这可能比它的价值更麻烦。 被警告。
下面是我尝试表示输入和 output 之间关系的尝试,其中考虑了输入类型的不确定性。 例如,如果调用abc("", {flagA: Math.random()<0.5}
,编译器应该返回类似FlagARet | NoFlagRet
的内容,因为它不能确定flagA
属性是true
还是false
。
首先,我创建了If
类型别名,它只执行条件类型检查,以保证它将分布在 unions中。
type If<X, Y, T, F> = X extends Y ? T : F;
现在是主要事件:
function abc<T extends Options | undefined = undefined>(name: string, options?: T):
T extends undefined ? NoFlagRet :
If<NonNullable<T>['flagA'], true, FlagARet,
If<NonNullable<T>['flagB'], true, FlagBRet, NoFlagRet>
>
function abc(name: string, options?: Options) {
if (options?.flagA) return { a: "ay" };
if (options?.flagB) return { b: "be" }
return { x: "ex" };
}
这有点涉及,但我会逐步介绍。 首先,在您不传入options
参数的情况下,泛型类型参数T
将默认为undefined
。 如果T
(或T
的任何联合成员) undefined
,则 output 类型将为NoFlagRet
(或NoFlagRet
将通过联合添加到 output 类型)。 然后,我们检查NonNullable<T>['flagA']
。 NonNullable<T>
类型确保我们忽略任何undefined
的部分,然后['flagA']
类型正在查找T
的flagA
属性。 如果该标志(或其任何联合成员)为true
,则我们返回FlagARet
(或FlagARet
将通过联合添加到 output 类型。)否则我们检查NonNullable<T>['flagB']
。 如果该标志(或等等等等)是true
,那么我们返回FlagBRet
(或等等添加联合等等)。 否则我们返回NoFlagRet
。
我已将 function 编写为单调用签名重载,因此实现类型检查更宽松,我可以返回FlagARet | FlagBRet | NoFlagRet
类型的值FlagARet | FlagBRet | NoFlagRet
FlagARet | FlagBRet | NoFlagRet
没有编译器抱怨。 希望您能看到实现与调用签名匹配。
好的,让我们测试一下:
console.log(abc("", { flagA: true }).a.toUpperCase()); // AY
console.log(abc("", { flagA: true, flagB: true }).a.toUpperCase()); // AY
console.log(abc("", { flagB: true }).b.toUpperCase()); // BE
console.log(abc("", {}).x.toUpperCase()); // EX
console.log(abc("", { flagA: true, flagB: false }).a.toUpperCase()); // AY
console.log(abc("", { flagA: false, flagB: true }).b.toUpperCase()); // BE
console.log(abc("").x.toUpperCase()); // EX
const aOrB = abc("", { flagA: Math.random() < 0.5, flagB: true });
// const aOrB FlagARet | FlagBRet
const aOrX = abc("", { flagA: Math.random() < 0.5 });
// const aOrX: FlagARet | NoFlagRet
const bOrX = abc("", { flagB: Math.random() < 0.5 });
// const bOrX: FlagBRet | NoFlagRet
const aOrBorX = abc("", { flagA: Math.random() < 0.5, flagB: Math.random() < 0.5 });
// const aOrBorX: FlagARet | FlagBRet | NoFlagRet
这一切都按我的预期工作。 没有编译器错误; 在每一步,编译器都会理解abc()
的 output 类型。 特别是,您可以看到,当我们开始传入类型为boolean
而不是已知的true
或false
的对象时,output 类型本身就是相关的联合。
这是完美的吗? 不可以。您始终可以将具有已知属性的值扩大到没有该属性的值。 例如,您可以将任何 object 类型分配给空的 object 类型{}
:
const flagA = {flagA: true};
let someObj: {};
someObj = flagA; // works because all objects are assignable to {}
这对我的abc()
实现来说是个坏消息:
abc("", someObj).x.toUpperCase(); // no compiler error, but runtime error!
// TypeError: abc(...).x is undefined
因为不再知道someObj
具有flagA: true
属性,所以编译器假定它没有,因此期望 output 类型为NoFlagRet
。 但是在运行时会出现一个FlagARet
,而我们有一个编译器没有捕捉到的运行时错误。
我可以修改abc()
的定义以更加谨慎,并假设其类型没有显式flagA
属性的值实际上可能在该属性处实际上具有true
值或false
值。 但这会使abc()
的 output 类型对常见用例不太有用。 这可能会得到缓解,并且存在一些最不坏的解决方案。
但这里的重点是,这种类型的杂耍并不容易做到,编译器并没有真正帮助你保持诚实。 您应该考虑是否真的需要这种功能,如果需要,请注意潜在的陷阱。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.