typescript 在对象的键前添加前缀只考虑重叠

yzuktlbb  于 2023-06-24  发布在  TypeScript
关注(0)|答案(1)|浏览(183)

我目前正在做一个servienow项目,这个项目显然不太关注DX。这就是为什么我想添加一些类型定义,使它更不容易出错。
基本上,我有数据库的表,可以使用类检索。语法看起来或多或少像new Query("table").where("field", fieldValue)fieldValue的类型取决于字段的类型。我正在提取表信息并将它们存储在 typescript 文件中。到目前为止,这是有效的。但是也有可能访问与表相关的字段,如下所示:new Query("table").where("ref.otherField", fieldValue)
类型可以无限嵌套,因为例如用户表可能有一个对管理器的引用,它再次指向用户表。这是我只想支持两个相关领域的方式,如果开发人员需要更多,我只想接受任何。
我的目标基本上是创建一个包含所有可能的字符串和相应值的类型,然后将此类型用于where函数。为此,我必须将引用字段的名称添加到引用表的字段中:

type AppendPrefix<prefix extends string, Object extends Record<string, any>> = {
    [key in `${prefix}.${keyof Object}`]: key extends `${prefix}.${infer Field}`
    ? Object[Field] extends Reference<any>
    ? sys_id | null
    : Object[Field]
    : never;
};

到目前为止,这对于给定的前缀和表也是有效的。但是如果我有两个字段和两个不同的表,我就不能让它工作。然后只取这些表的重叠部分,例如,如果两个表都有一个名为name的字段,我会得到如下内容:

{
  ...
  "prefix1.name": string;
  "prefix2.name": string;
}

我尝试像这样获取所有前缀类型:

type AddReferenceTable<Table extends keyof Tables.List, Field extends keyof Tables.List[Table]> = AppendPrefix<
    Field,
    GetReferenceTable<Table, Field>
>;

type GetReferenceTable<
    Table extends keyof Tables.List,
    Field extends keyof Tables.List[Table]
> = Tables.List[Table][Field] extends Reference<infer T>
    ? Tables.List[T]
    : never;

下面是一个完整的代码示例:Playground
有人能告诉我我做错了什么吗?或者我试图解决问题的方法很糟糕,有更好的方法吗?
谢谢你!

mbskvtky

mbskvtky1#

因为你的实体引用的是自己的(User.manager: User),这将导致无限递归,唯一合适的解决方案是限制递归的深度。您将需要根据您的用例来评估此值,但对于此解决方案,限制为2
让我们从算法开始:

  • 假设ObjWithNoReference是一个初始类型,没有引用其他表的属性
  • 如果达到深度限制
  • 返回ObjWithNoReference
  • 埃尔瑟
  • Map初始类型的键(不包括ObjWithNoReference的键),以确保我们只遍历引用的字段。
  • 递归地为表的相关类型调用实用程序类型,并增加深度级别。
  • 用键的名称作为被引用表的被调用实用工具类型的结果的前缀。
  • 包括{keyName: sys_id | null}
  • 返回ObjWithNoReference和引用的表数据

实施:
公用事业:
Prettify-使IDE显示的类型更具可读性和线性(删除&,. etc)

type Prettify<T> = T extends infer R
  ? {
      [K in keyof R]: R[K];
    }
  : never;

ValueOf-返回传递类型的所有值的并集

type ValueOf<T> = T[keyof T];

UnionToIntersection-将传递的并集转换为交集。解释here

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I,
) => void
  ? I
  : never;

DepthLimit-表示最大参考深度的数字:

type DepthLimit = 2;

深度检查机制说明:我们要递归调用的类型将接受unknown[]的额外泛型参数,默认为[]。在每次递归调用时,我们都要将一个元素推到数组中,以增加它的长度。如果数组的长度等于DepthLimit,则停止递归调用。
所需类型:
首先,让我们使用mapped type选择不是对另一个表的引用的属性:

{
    [K in keyof T as T[K] extends Reference<any> ? never : K]: T[K];
} extends infer ObjWithNoReference ? doSomething : neverWillBeReached

看看我们是否需要再深入一点。如果达到限制,则返回没有引用的字段:

Depth['length'] extends DepthLimit
      ? ObjWithNoReference
      : needToGoRecursively

为了检索引用其他表的字段,我们将使用Map类型,而初始类型具有省略键ObjWithNoReference

{
  [K in Exclude<keyof T, keyof ObjWithNoReference>]: T[K] extends Reference<
    infer TableName extends keyof Tables.List> 
     ? callRecursively
     : never // won't be reached
}

如果T[K]是一个引用,我们检索该able的名称,并将其放入infer变量TableName中,并调用该名称的表DesiredType,深度级别增加。将结果放入ReferencedTable

DesiredType<Tables.List[TableName],[...Depth, unknown]> extends infer ReferencedTable 
? // do something
: never // won't be reached

ReferencedTable的预期形状为Record<string, unknown>,其中键是表的键。但是,我们需要用当前表的键和.作为这些键的前缀。由于我们必须在当前表的键下添加sys_id,我们将手动添加它与交集:

{
    [P in keyof ReferencedTable as `${K & string}.${P &
      string}`]: ReferencedTable[P];
} & { [P in K]: sys_id | null }

到目前为止,代码将给出类似于以下内容(仅包括引用字段):

type Test = {
    asset: {
        asset: sys_id | null;
    } & {
        "asset.name": string;
        "asset.id": number;
    };
    manager: {
        manager: sys_id | null;
    } & {
        "manager.name": string;
        "manager.priority": number;
    }
}

我们需要得到assetmanager的值,而不是整个对象,因为我们想要平面结构。我们将使用ValueOf
如果我们将前面代码块中的示例结果传递给ValueOf,我们将得到:

// ({
//   asset: sys_id | null;
// } & {
//   "asset.name": string;
//   "asset.id": number;
// }) | ({
//   manager: sys_id | null;
// } & {
//   "manager.name": string;
//   "manager.priority": number;
// })
ValueOf<Test>

这不是预期的,因为ValueOf返回值的并集,我们需要使用UnionToIntersection将其变成单个对象:

// {
//   asset: sys_id | null;
// } & {
//   'asset.name': string;
//   'asset.id': number;
// } & {
//   manager: sys_id | null;
// } & {
//   'manager.name': string;
//   'manager.priority': number;
// }
UnionToIntersection<ValueOf<Test>>

这是需要的结构,但很难读懂。让我们用Prettify改进一下:

// {
//     asset: sys_id | null;
//     'asset.name': string;
//     'asset.id': number;
//     manager: sys_id | null;
//     'manager.name': string;
//     'manager.priority': number;
// }
Prettify<UnionToIntersection<ValueOf<Test>>>

看起来很棒!现在让我们把这一切放在一起:

type DesiredType<T, Depth extends unknown[] = []> = Prettify<
  {
    [K in keyof T as T[K] extends Reference<any> ? never : K]: T[K];
  } extends infer ObjWithNoReference
    ? Depth['length'] extends DepthLimit
      ? ObjWithNoReference
      : ObjWithNoReference &
          UnionToIntersection<
            ValueOf<{
              [K in Exclude<
                keyof T,
                keyof ObjWithNoReference
              >]: T[K] extends Reference<
                infer TableName extends keyof Tables.List
              >
                ? DesiredType<
                    Tables.List[TableName],
                    [...Depth, unknown]
                  > extends infer ReferencedTable
                  ? { [P in K]: sys_id | null } & {
                      [P in keyof ReferencedTable as `${K & string}.${P &
                        string}`]: ReferencedTable[P];
                    }
                  : never
                : never;
            }>
          >
    : never
>;

测试:

// {
//   name: string;
//   priority: number;
//   asset: sys_id | null;
//   "asset.name": string;
//   "asset.id": number;
//   manager: sys_id | null;
//   "manager.name": string;
//   "manager.priority": number;
//   "manager.asset": sys_id | null;
//   ... 4 more ...;
//   "manager.manager.priority": number;
// }
type Test = DesiredType<Tables.User>

现在我们需要在Query.where中调用这个类型:

type Query<Table extends keyof Tables.List> = {
  where<
    PathObject extends DesiredType<Tables.List[Table]>,
    Path extends keyof PathObject,
  >(
    fieldName: Path,
    value: PathObject[Path],
  ): Query<Table>;
};
new Query('user')
  .where('name', 'Josef')
  .where('manager', null)
  .where('manager.name', 'Yossarian')
  .where('manger.priority', 5) // expected error
  .where('unknownField', 'someValue') // expected error
  .where('name', 5); // expected error

链接到Playground

相关问题