这与使用3Plib中的类型有关。
我希望知道是否有一种快速的方法来缩小所有已知的子树,这样我就不需要单独地断言每个字段和子字段了?
interface User {
id: string;
private?: Private;
}
interface Private {
info?: Info;
images?: ImageData[];
// ...
}
interface Info {
name?: string;
foo?: Foo;
}
// etc
async function client_list(id: string, parts: string[]) {
// retrieve some user data
// if successful, is guaranteed to return a known tree
// eg for parts == ["info"]
const user = {
id: id,
private: {
info: {
foo: {
bar: {
some_number: 42
}
}
}
}
} as User;
return user;
}
(async () => {
const user = await client_list("abcd", ["info"]);
user.private.info.foo.bar.some_number; // private, info, foo and bar are possibly undefined
// after somehow narrowing the subtree down to some_mumber, asserting each node is not undefined
user.private.info.foo.bar.some_number; // everything would be fine
})();提前谢谢你。
Ps:我认为复制接口但没有可选属性会很好,但这将意味着复制大量的接口。
发布于 2021-05-19 02:29:05
您可以使用递归映射和有条件的来获取对象类型T和对应于该对象的索引路径的元组 KS,并将其转换为一个新类型,在该类型中,路径KS处的T子树被替换为其自身的一个版本,在该版本中,所有属性和子属性都是必需的,而不是可选的。
但它并不特别漂亮。
如果您想要做的只是将T转换成一个版本,其中所有的属性、子属性、子属性等等都是必需的,而不是可选的,那么您可以很容易地做到这一点:
type DeepRequired<T> =
T extends Function ? T : { [K in keyof T]-?: DeepRequired<T[K]> }在这里,我们将函数单独放在一边(映射类型对函数没有好处,所以最好不要修改它们);对于非函数类型,我们只是使用-? 映射类型修饰符将任何可能的可选属性转换为所需的属性,而我们则向下递归。
让我们看看这对您的一个接口造成了什么影响:
type DeepRequiredInfo = DeepRequired<Info>
/* type DeepRequiredInfo = {
name: string;
foo: {
bar: {
some_number: number;
};
};
} */您可以看到,name和foo属性DeepRequired<Info>都是必需的。name的类型是string,而foo的类型是DeepRequired<Foo>,它的属性是必需的,依此类推。
注意,DeepRequired<ImageData>也会向下遍历它的属性,使它们都是必需的,这可能不是您想要的。但我会考虑在这个问题的范围之外做任何不同的事情。就目前而言,这只是一个潜在的警告。
当然,DeepRequired并不完全是您想要的;您想要的是类似于RequireSubtree<User, ["private", "info"]>这样的东西,其中只有User的private属性是必需的,只有private属性的info属性是必需的,而且该属性的类型是‘`DeepRequired’。
好吧,这就是我们该怎么做的。首先,编写一个名为Expand<T>的类型函数很有用,它接受像{a: string} & {b: number}这样的对象类型的丑陋的交叉口,并将它们转换为{a: string; b: number}等等效的单个对象类型,并且倾向于将嵌套类型函数(如Baz<Qux<Fnord>> )转换为具有明确写入属性的等效对象类型;有关更多信息,请参见这个问题:
type Expand<T> = T extends infer U ? { [K in keyof U]: T[K] } : never;现在,有趣的是:
type RequireSubtree<T, KS extends readonly any[]> =
T extends Function ? (
T
) : (
KS extends readonly [infer K, ...infer R] ? (
[K] extends [never] ? T :
[K] extends [keyof T] ? (
Expand<
Omit<T, K> &
{ [P in K]-?: Exclude<RequireSubtree<T[P], R>, undefined> }
>
) : (
T
)
) : (
DeepRequired<T>
)
);同样,如果T是一个函数类型,我们不想改变它。
然后我们来看一下元组KS的路径。如果它不是空的,我们使用它的第一个元素K,并保留其余的元组R。如果K是T的一个键,那么我们希望使用没有K子树的T (使用实用程序类型),并将其与一个版本的K子树交叉,该子树的属性键都是必需的,其属性值从它们中得到undefined D,其属性值已通过RequireSubTree递归地使用路径元组R运行。
如果路径元组是空的,那么我们最终会沿着完整的路径走下去,我们可以返回DeepRequired<T>。
有一些奇怪的边缘案件正在处理中,我不会进入,而且可能有更奇怪的边缘案件,没有处理,我也不会进入。所以有大量的警告。
但现在让我们确保它能做正确的事情:
type UserWithDeepRequiredPrivateInfo = RequireSubtree<User, ["private", "info"]>
/* type UserWithDeepRequiredPrivateInfo = {
id: string;
private: {
images?: ImageData[] | undefined;
info: {
name: string;
foo: {
bar: {
some_number: number;
};
};
};
};
} */这看起来很好;private属性是必需的,它的类型是一个非常需要的info子树,但是images属性仍然是可选的,这是需要的。
现在,我们可以给您的client_list函数提供一个类型签名:
async function client_list<K extends keyof Private>(id: string, parts: K[]):
Promise<RequireSubtree<User, ["private", K]>> {
throw new Error("needs to be properly implemented");
}parts列表似乎是Private的键数组,而client_list的返回类型是Promise of RequireSubtree<User, ["private", K]>>。请注意,正确地实现此函数需要注意,因为编译器不可能验证为泛型Promise<RequireSubtree<User, ["private", K]>>分配的任何内容;您将需要一个类型断言或类似的东西来抑制编译器错误。
无论如何,我们终于可以调用client_list,看看它是如何工作的:
const user = await client_list("abcd", ["info"]);
/* const user: const user: {
id: string;
private: {
images?: ImageData[] | undefined;
info: {
name: string;
foo: {
bar: {
some_number: number;
};
};
};
};
} */
user.private.info.foo.bar.some_number; // okay
user.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined这看起来很好;info属性有一个完全必需的子树,而images属性仍然可能是undefined。
将其与以下内容进行比较:
const otherUser = await client_list("efgh", ["images"]);
otherUser.private.info.foo.bar.some_number; // error, possibly undefined
otherUser.private.images.map(iD => iD.height.toFixed(2)); // okay
const thirdUser = await client_list("ijkl", ["images", "info"]);
thirdUser.private.info.foo.bar.some_number; // okay
thirdUser.private.images.map(iD => iD.height.toFixed(2)); // okay
const nilUser = await client_list("mnop", []);
nilUser.private.info.foo.bar.some_number; // error, possibly undefined
nilUser.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined我觉得这是你想要的行为,对吧?
注意,如果我认为您只会使用一个或两个层次深的子树路径,那么我可能不会建议使用一个完全递归的RequiredSubtree。相反,我可能只会使用DeepRequired和一些private和info的手工输入。但我假设这些只是示例,您的实际用例可能在某些任意深度上具有所需的子树。
最后,您应该考虑完全递归映射的条件类型是否值得这么复杂;您刚才提到的手动版本很乏味,但概念上很简单;任何查看您的自定义接口的人都可能能够理解他们做什么,以及在出了问题时如何调试它,而有些人则在查看RequiredSubtree<Foo, ["bar", "baz", "qux"]>,并试图找出为什么在一段艰难的时间内它并不是他们所期望的那样。
https://stackoverflow.com/questions/67589864
复制相似问题