TypeScript:根据递归输入类型自动推断函数输出类型

ibps3vxo  于 2023-10-22  发布在  TypeScript
关注(0)|答案(1)|浏览(100)

我正在尝试用TypeScript为图形数据库写一个ORM。具体来说,就是“find”方法,它将返回一个特定实体的列表。现在,还可以向该函数传递一个与应该在数据库级别上完成的连接相关的结构。理想情况下,函数应该自动输入这些附加字段,以便客户端可以访问它们。只有一个层次的嵌套,我已经能够完成这一点,虽然这将是可怕的,使它与多个层次的工作。
我的(已经工作的)解决方案只有一个嵌套级别如下:

interface IDocumentModel {
  _id?: string;
}

type JoinParams<T extends Record<string, IDocumentModel>> = {
  [K in keyof T]: {
    model: DocumentModel<T[K]>;
  };
};

type JoinResult<T, U> = (U & {
  [K in keyof T]: T[K][];
})[];

class DocumentModel<T extends IDocumentModel> {
  async find<X extends Record<string, IDocumentModel>>(
    filter?: Partial<T>,
    hydrate?: JoinParams<X>,
  ): Promise<JoinResult<X, T>> {
    // TODO: implementation
  }
}

const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();

const results = await ParentModel.find(
  { _id: 'abc' },
  {
    children: {
      model: ChildModel,
    },
  },
);

console.log(results[0].parentField);
console.log(results[0].children[0].childField);

现在的挑战是将其扩展到两个级别,甚至更好的是任意数量的级别。这是我正在进行的两层嵌套的工作:
以下是我目前围绕这个问题的解决方案。

interface IDocumentModel {
  _id?: string;
}

type JoinParams<
  T extends
    | Record<string, IDocumentModel>
    | Record<string, Record<string, IDocumentModel>>,
> = {
  [K in keyof T]: {
    model: T extends Record<string, Record<string, IDocumentModel>>
      ? DocumentModel<T[K]['parent']>
      : T extends Record<string, IDocumentModel>
      ? DocumentModel<T[K]>
      : never;
    hydrate?: T extends Record<string, Record<string, IDocumentModel>>
      ? JoinParams<Omit<T[K], 'parent'>>
      : never;
  };
};

type JoinResult<
  T extends
    | Record<string, IDocumentModel>
    | Record<string, Record<string, IDocumentModel>>,
  U,
> = (U & {
  [K in keyof T]: T extends Record<string, Record<string, IDocumentModel>>
    ? JoinResult<Omit<T[K], 'parent'>, T[K]['parent']>
    : T extends Record<string, IDocumentModel>
    ? T[K][]
    : never;
})[];

class DocumentModel<T extends IDocumentModel> {
  async find<X extends Record<string, Record<string, IDocumentModel>>>(
    filter?: Partial<T>,
    hydrate?: JoinParams<X>,
  ): Promise<JoinResult<X, T>> {
    // TODO: implementation
  }
}

const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const GrandChildModel = new DocumentModel<{ _id?: string, grandChildField: string }>();

const results = await ParentModel.find(
  { _id: 'abc' },
  {
    children: {
      model: ChildModel,
      hydrate: {
        grandchildren: {
          model: GrandChildModel,
        },
      },
    },
  },
);

console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);

当我尝试我的测试案例时

console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);

我没有得到任何超过results[0].parentField的自动完成。这意味着IDE不再建议将results[0].children作为有效字段。
我希望这是足够的信息,虽然我很乐意澄清更多,如果不清楚。

omqzjyyz

omqzjyyz1#

JoinParams是递归conditional type时,TypeScript不够聪明,无法从JoinParams<T>类型的hydrate值推断generic类型参数T。类型函数F<T>越复杂,编译器就越不可能从中推断出T。您应该重构hydrate参数,使其类型与您试图推断的泛型类型参数非常简单地相关,而不是尝试这样做。最简单的关系是身份:如果你想从hydrate中推断出H,那么直接将hydrate的类型设为H。然后你可以从H * 计算 * 你的其他类型。
一种方法看起来像:

class DocumentModel<M extends IDocumentModel> {
  declare t: M;
  async find<H extends Hydrate>(
    filter?: Partial<M>,
    hydrate?: H,
  ): Promise<Find<M, H>> {
    hydrate?.children.hydrate?.grandchildren.model;
    return null!
  }
}

type Hydrate = {
  [k: string]: SubHydrate<IDocumentModel, Hydrate>
}

interface SubHydrate<M extends IDocumentModel, H extends Hydrate> {
  model: DocumentModel<M>,
  hydrate?: H
}    

type Find<M extends IDocumentModel, H extends Hydrate> = Array<M & {
  [K in keyof H]: H[K] extends 
    SubHydrate<infer M extends IDocumentModel, infer H extends Hydrate> ? Find<M, H> : never
}>

在这里,我们将H约束为Hydrate,这种类型应该允许有效值而不允许无效值。Hydrate是按照SubHydrate编写的,SubHydrate本身在模型类型M和嵌套的Hydrate类型H中是通用的。所以H应该很容易从对find()的调用中推断出来。
然后find()的返回类型是Find<M, H>,它完成了将hydratemodel的类型转换为预期输出类型的工作。它通过H递归下降,从它推断嵌套的MH类型。
让我们看看它的实际操作:

const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const GrandChildModel = new DocumentModel<{ _id?: string, grandChildField: string }>();

const results = await ParentModel.find(
  { _id: 'abc' },
  {
    children: {
      model: ChildModel,
      hydrate: {
        grandchildren: {
          model: GrandChildModel,
        },
      },
    },
  },
);

console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);

看上去不错。一切都如你所愿。
Playground链接到代码

相关问题