在下面的代码示例中,我希望我的 IDE 接受
mySap.rootValues[0].test2
和 mySap.rootValues[1].test4
为有效。现在还没有,因为 rootValues[0]
和 rootValues[1]
属于 TestRootOutput | TestRootOutput2
类型。
type RootFunction<T, U> = (input: T) => U;
type Root<T, U> = {
value: U;
};
const root = <T, U>(rootFn: RootFunction<T, U>): ((initialValue: T) => Root<T, U>) => {
return initialInput => {
let _value: U = rootFn(initialInput);
return {
get value() {
return _value;
},
};
};
};
interface TestRootInput {
test: string;
}
interface TestRootOutput {
test2: string;
}
interface TestRootInput2 {
test3: string;
}
interface TestRootOutput2 {
test4: string;
}
const testRoot = root<TestRootInput, TestRootOutput>(input => {
return {test2: input.test};
});
const testRoot2 = root<TestRootInput2, TestRootOutput2>(input => {
return {test4: input.test3};
});
type BranchSap = {roots: Root<any, any>[]};
const sap = <T extends BranchSap>(sap: T) => {
const roots = sap.roots;
return {
rootValues: roots.map((root) => root.value as any as T['roots'][number]['value']),
};
};
const mySap = sap({roots: [testRoot({test: 'hi'}), testRoot2({test3: 'hi2'})]});
// should be valid
console.log(mySap.rootValues[0].test2)
// should be valid
console.log(mySap.rootValues[1].test4)
您的
sap()
函数的返回类型为 {rootValues: Array<T['roots'][number]['value']>}
类型。但是 T['roots'][number]
是 roots
数组的所有元素类型的 union,它完全无法追踪哪些值在数组中的位置,并且
Array
是无序同构数组类型,并且不对可以在哪个索引处找到哪些类型进行编码。所以你已经抛弃了 T['roots']
中有关元素长度和位置的信息。
相反,您需要
rootValues
成为依赖于 T['roots']
的元组类型。首先,我们要确保
T['roots']
可能会被推断为一个元组。为此,我们可以将 T
设为 const
类型参数:
const sap = <const T extends BranchSap>(sap: T): { ⋯ } => ⋯;
然后,我们想要获取
T['roots']
并将其 映射到新的元组类型。也就是说,我们创建一个 mapped type 将 I
的索引 T['roots']
处的每个元素转换为 T['roots'][I]['value']
:
type RootVal<T extends Root<any, any>[]> =
{ [I in keyof T]: T[I]['value'] };
const sap = <const T extends BranchSap>(
sap: T
): { rootValues: RootVal<T['roots']> } => ⋯;
我们从调用者的角度来测试一下:
const mySap = sap({
roots: [
testRoot({ test: 'hi' }),
testRoot2({ test3: 'hi2' })
]
});
如果您使用 IntelliSense 检查,您会看到
T
被推断为
{ readonly roots: [
Root<TestRootInput, TestRootOutput>,
Root<TestRootInput2, TestRootOutput2>
];
}
由于
const
类型参数。因此输出的类型为
{ rootValues: [TestRootOutput, TestRootOutput2]; }
因为这就是当您使用
RootVal
映射输入元组时得到的结果。现在 mySap
的行为完全如您所愿:
mySap.rootValues[0].test2.toUpperCase(); // okay
mySap.rootValues[1].test4.toUpperCase(); // okay
请注意,这与sap()
的
实现关系不大。大多数情况下,您只需要使用类型断言来说服编译器该实现满足调用签名:
const sap = <const T extends BranchSap>(
sap: T
): { rootValues: RootVal<T['roots']> } => {
const roots = sap.roots;
return {
rootValues: roots.map((root) => root.value),
} as any; // <-- assert here or wherever you want
};
map()
数组方法的 TypeScript 类型不会生成元组类型。事实上,让 TypeScript 自动完成这样的事情是不可能的,因为 TypeScript 类型系统缺乏必要的表达能力来表示它,请参阅将元组类型值映射到不同的元组类型值而不进行强制转换。
在实践中,大多数通用函数无法被编译器验证为安全,因为它缺乏分析除少数特定操作(如索引)之外的所有操作的能力。因此,您通常需要类型断言等。