typescript - 递归构建 typescript 类型

标签 typescript typescript-generics

是否可以通过递归查找基于字符串数组输入生成泛型类型。

我想要类似下面示例的内容:

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/

相关文章:

html - 当鼠标输入时,它会在所有元素名称上显示图标

javascript - 如何使用 dgeni 解析 typescript 文件中的文档注释

typescript - TypeScript 中的柯里化(Currying)

arrays - 制作通用接口(interface)数组

javascript - Monaco Editor 中 "this"的自定义 javascript 代码完成

typescript - 使用 SubSink 而不是 Subscriptions 数组有什么意义

typescript - 在 typescript 中,索引签名标识符有什么用?

typescript - 从联合类型中提取,其中鉴别器也是联合

typescript - 实例化类的通用 TypeScript 函数

javascript - 带有组件的 Typescript 复杂泛型