为了具体起见,我将定义这些接口:
interface FlagARet {
a: string;
}
interface FlagBRet {
b: string;
}
interface NoFlagRet {
x: string;
}
如果options 将flagA 设置为true,则abc() 将返回FlagARet 值;否则,如果options 将flagB 设置为true,则abc() 将返回FlagBRet 值;否则 abc() 将返回 NoFlagRet 值。
这种类型的杂耍不是编译器会自动为你做的,甚至无法在编译时验证;您需要向编译器准确说明您要评估的类型函数,并执行与 type assertion 等效的操作来告诉编译器您保证该函数实际上返回您的类型声称。这很难真正保证。可能存在边缘情况,因此这可能比它的价值更麻烦。请注意。
下面是我在考虑输入类型不确定性的情况下表示输入和输出之间关系的尝试。例如,如果调用abc("", {flagA: Math.random()<0.5},编译器应该返回类似FlagARet | NoFlagRet 的内容,因为它不能确定flagA 属性是true 还是false。
首先我创建了If 类型别名,它只执行conditional type 检查,以确保它将是distributed across 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 将default 变为undefined。如果T(或T 的任何联合成员)为undefined,则输出类型将为NoFlagRet(或NoFlagRet 将通过联合添加到输出类型中)。然后,我们检查NonNullable<T>['flagA']。 NonNullable<T> 类型确保我们忽略任何undefined 部分,然后['flagA'] 类型是looking up T 的flagA 属性。如果该标志(或它的任何联合成员)是true,那么我们返回FlagARet(或FlagARet 将通过联合添加到输出类型。)否则我们检查NonNullable<T>['flagB']。如果那个标志(或等等等等)是true,那么我们返回FlagBRet(或者等等等等添加联合等等)。否则我们返回NoFlagRet。
我已将函数编写为单一调用签名overload,因此实现类型检查更松散,并且我可以返回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() 的输出类型。特别是,您可以看到,当我们开始传入类型为boolean 而不是已知的true 或false 的对象时,输出类型本身就是相关的联合。
这是完美的吗? 否。 您始终可以将具有已知属性的值扩大到没有该属性的值。例如,您可以将任何对象类型分配给空对象类型{}:
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 属性,因此编译器假定它没有,因此期望输出类型为NoFlagRet。但是在运行时会出现 FlagARet,而我们遇到了编译器没有捕获的运行时错误。
我可以修改 abc() 的定义以更加谨慎,并假设其类型没有显式 flagA 属性的值实际上可能具有 true 或 false 值无论如何在那个属性。但这会使abc() 的输出类型对常见用例不太有用。这可能会得到缓解,并且存在一些最不坏的解决方案。
但这里的重点是,这种类型的杂耍并不容易做到,编译器并没有真正帮助你保持诚实。您应该考虑一下您是否真的需要这种功能,如果需要,请注意潜在的陷阱。
Playground link to code