是否可以通过递归查找基于字符串数组输入生成泛型类型。
我想要类似下面示例的内容:
type Author = {
id: number;
name: string;
articlesAbout?: Person[];
friends: Author[];
};
type Person = {
id: number;
name: string;
children?: Person[];
author?: Author;
}
type TheRoot = {
username: string;
persons: Person[];
};
// I want the return type of this to be a calculated type by the input strings.
function myFactory(inputs: string[]) {
return {};
}
// This should give a type that looks like this:
// {
// username: TheRoot['username'];
// persons: {
// id: TheRoot['persons']['0']['id'];
// name: TheRoot['persons']['0']['name'];
// }[],
// }
const calculated = myFactory(['username', 'persons.id', 'persons.name']);
// Fail as 'id' doesn't exist
const shouldGiveError = myFactory(['id', 'username']);
// Anything inside parenthesis should just be ignored when doing the lookup
// This would return
// {
// id: TheRoot['id'];
// username: TheRoot['username'];
// }
const ignoreParenthesis = myFactory(['id', 'username(asLowerCase: true)']);
我猜我正在寻找一些使用 infer
关键字的解决方案,而 myFactory
函数实际上不会接受 string[]
,而是一些通用类型,然后将用于构建此计算返回类型。
最佳答案
好吧,让我们从函数签名开始。
type Expand<T> =
T extends ((...args: any[]) => any) | Map<any, any> | Set<any> | Date | RegExp
? T
: T extends ReadonlyArray<unknown>
? `${bigint}` extends `${keyof T & any}`
? { [K in keyof T]: Expand<T[K]>; }
: Expand<T[number]>[]
: T extends object
? { [K in keyof T]: Expand<T[K]> }
: T;
type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });
function myFactory<Inputs extends string[]>(inputs: Valid<Narrow<Inputs>>): Expand<Construct<Inputs>> { ... }
虽然已经很困惑了,但实际上很容易理解。
我会访问 Valid
稍后键入,但现在它所做的只是通过通用参数 Inputs
获取输入,将其缩小到精确值而不是 string[]
(因此不需要 as const
),然后返回输入的构造。
由于该构造将为我们提供非常长的类型交集,因此我们使用 Expand
将其简化为一种对象类型。
Narrow
很难掌握,但您可以通过 this 了解它的工作原理。回答者不是别人,正是 jcalz。
获得输入后,我们需要将其分割为 .
(并忽略括号)。
type SplitPath<Key, Path extends ReadonlyArray<unknown> = []> =
Key extends `${infer A}.${infer B}`
? A extends `${infer A}(${string}`
? SplitPath<B, [...Path, A]>
: SplitPath<B, [...Path, A]>
: Key extends `${infer Key}(${string}` // if parenthesis ignore after it
? [...Path, Key]
: [...Path, Key];
// "undo" splitting
type JoinPath<P, S extends string = ""> = P extends [infer First, ...infer Rest] ? JoinPath<Rest, `${S}${S extends "" ? "" : "."}${First & string}`> : S;
type KeyPaths<Inputs extends string[]> = {
[K in keyof Inputs]: SplitPath<Inputs[K]>;
};
这些类型的作用正如它们听起来的那样。 KeyPaths
只是一个用于分割元组中所有路径的实用程序(可以更好地命名)。
你准备好迎接 Construct
了吗?现在输入吗?
type Construct<Inputs extends string[], T = TheRoot> = {
[K in KeyPaths<Inputs>[number][0] as K extends keyof T ? T[K] extends NonNullable<T[K]> ? K : never : never]:
K extends keyof T
? OmitFirstLevel<KeyPaths<Inputs>, K> extends []
? T[K] extends object
? T[K] extends ReadonlyArray<unknown>
? never[]
: Record<never, never>
: T[K]
: T[K] extends ReadonlyArray<unknown>
? Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, T[K][number]>[]
: Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, T[K]>
: never;
} & {
[K in KeyPaths<Inputs>[number][0] as K extends keyof T ? T[K] extends NonNullable<T[K]> ? never : K : never]?:
K extends keyof T
? OmitFirstLevel<KeyPaths<Inputs>, K> extends []
? NonNullable<T[K]> extends object
? NonNullable<T[K]> extends ReadonlyArray<unknown>
? never[]
: Record<never, never>
: NonNullable<T[K]>
: NonNullable<T[K]> extends ReadonlyArray<unknown>
? Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, NonNullable<T[K]>[number]>[]
: Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, NonNullable<T[K]>>
: never;
};
我也不是。但我们只需要关注前半部分:
{
[K in KeyPaths<Inputs>[number][0] as K extends keyof T ? T[K] extends NonNullable<T[K]> ? K : never : never]:
K extends keyof T
? OmitFirstLevel<KeyPaths<Inputs>, K> extends []
? T[K] extends object
? T[K] extends ReadonlyArray<unknown>
? never[]
: Record<never, never>
: T[K]
: T[K] extends ReadonlyArray<unknown>
? Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, T[K][number]>[]
: Construct<OmitFirstLevel<KeyPaths<Inputs>, K>, T[K]>
: never;
}
本质上,我们只是获取所需的键,然后递归地构造这些键的类型。如果没有为数组或对象类型提供更多键,则输出为 never[]
或Record<never, never>
。您从未说出您的期望,但如果您想要整个类型,请替换 never[]
和Record<never, never>
与 T[K]
.
如果要构造的类型是数组,我们获取元素的类型并构造它们,然后将其包装回数组(展开、修改,然后重新包装)。
此类型的后半部分执行相同的操作,但针对可选键。这样,就保留了 key 的“可选性”。
最后,这是OmitFirstLevel
:
type OmitFirstLevel<
Keys,
Target extends string,
R extends ReadonlyArray<unknown> = [],
> = Keys extends readonly [infer First, ...infer Rest]
? First extends readonly [infer T, ...infer Path]
? T extends Target
? Path extends []
? OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, [...R, JoinPath<Path>]>
: OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, R>
: R;
它所做的事情与我在评论中链接的问题/答案中的类型完全相同,只是对类型进行了一些小的更改以适应我们的用例。
不过,我们不能忘记验证我们的输入。
type Valid<Inputs extends string[], O = TheRoot, T = Required<O>> = KeyPaths<Inputs>[number][0] extends keyof T ? Inputs : {
[K in keyof Inputs]:
SplitPath<Inputs[K]>[0] extends keyof T
? Inputs[K]
: { error: `'${Inputs[K]}' is not a valid key.` };
};
不幸的是,我找不到一种方法来验证嵌套键,只能验证第一层,而不删除Narrow
的便利性。 (因为如果输入“太复杂”,它就无法正确推断输入)。
但是它很酷,因为它可以准确地告诉您哪个 key 无效。该错误仅出现在无效 key 下。
const shouldGiveError = myFactory(['id', 'username']);
// ~~~~ 'id' is not a valid key
它仍然适用于复杂的有效输入:
const veryComplicated = myFactory([
"username",
"persons.id",
"persons.name",
"persons.children.id",
"persons.children.name",
"persons.author.id",
"persons.author.name",
"persons.author.friends.id",
]);
const unrealistic = myFactory([ // get their great-great-great-great-great-grandchildren
"persons.children.children.children.children.children.children"
]);
如果这个解决方案有不良行为,我有义务回来修补它,现在分道扬镳,playground供您修改。
附注我特别恼火的是,我无法让验证适用于所有级别,但我想在检查了 @Filly 的答案后,我可以让它稍后工作。
关于typescript - 递归构建 typescript 类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73455767/