具有映射类型和条件类型的递归类型定义 [英] Recursive type definition with mapped and conditional types

查看:21
本文介绍了具有映射类型和条件类型的递归类型定义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在努力想出一种使用TypeORM提高类型安全性的方法。下面是一些TypeORM实体定义示例。

import { BaseEntity, Entity, Column, ManyToMany, JoinTable, ManyToOne, OneToMany } from 'typeorm';

@Entity()
class Product extends BaseEntity {
  @Column({ type: 'text' })
  public name: string;

  @Column({ type: 'text' })
  public description: string;

  @ManyToMany(_ => Category, category => category.products)
  @JoinTable()
  public categories: Category[];
}

@Entity()
class Category extends BaseEntity {
  @Column({ type: 'text' })
  public name: string;

  @ManyToMany(_ => Product, product => product.categories)
  public products: Product[];

  @ManyToOne(_ => Supplier, supplier => supplier.categories, { nullable: false })
  public supplier: Supplier;
}

@Entity()
class Supplier extends BaseEntity {
  @Column('text')
  public name: string;

  @Column({ type: 'boolean', default: true })
  public isActive: boolean;

  @OneToMany(_ => Category, category => category.supplier)
  public categories: Category[];
}

我正在尝试定义一种类型,该类型仅对实体本身的实体属性有效。这最好用一个例子来解释:

type Relations<T extends BaseEntity> = {
  // An object whose:
  // - Keys are some (or all) of the keys in type T, whose type is something which extends BaseEntity.
  // - Values are another Relations object for that key.
}

// Some examples

// Type error: "color" is not a property of Product.
const a: Relations<Product> = {
  color: {}
}

// Type error: "name" property of Product is not something that extends "BaseEntity".
const a: Relations<Product> = {
  name: {}
}

// OK
const a: Relations<Product> = {
  categories: {}
}

// Type error: number is not assignable to Relations<Category>
const a: Relations<Product> = {
  categories: 42
}

// Type error: "description" is not a property of Category.
const a: Relations<Product> = {
  categories: {
    description: {}
  }
}

// Type error: "name" property of Category is not something that extends "BaseEntity".
const a: Relations<Product> = {
  categories: {
    name: {}
  }
}

// OK
const a: Relations<Product> = {
  categories: {
    supplier: {}
  }
}

// Type error: Date is not assignable to Relations<Supplier>
const a: Relations<Product> = {
  categories: {
    supplier: new Date()
  }
}

// etc.

到目前为止,我得出了以下结论,但它不起作用,甚至可能与正确答案相距甚远:

type Flatten<T> = T extends Array<infer I> ? I : T;

type ExcludeNonEntity<T> = T extends BaseEntity | Array<BaseEntity> ? Flatten<T> : never;

type Relations<T extends BaseEntity> = {
  [P in keyof T as ExcludeNonEntity<P>]: Relations<T[P]>;
};

推荐答案

我的建议如下:

type DrillDownToEntity<T> = T extends BaseEntity ?
    T : T extends ReadonlyArray<infer U> ? DrillDownToEntity<U> : never;

type Relations<T extends BaseEntity> =
    { [K in keyof T]?: Relations<DrillDownToEntity<T[K]>> }
DrillDownToEntity<T>类似于您的Flatten<T>类型与ExcludeNonEntity<T>混合,不同之处在于它是递归操作的。它为任意数量的嵌套提取所有数组元素类型,只保留那些可分配给BaseEntity的类型。观察:

type DrillTest = DrillDownToEntity<Category | string | Product[] | Supplier[][][][][]>
// type DrillTest = Category | Product | Supplier

我不知道您是否会有数组的数组;如果没有,您可以将其设置为非递归的。但重要的是,不能最终赋值给BaseEntity的任何类型都将被丢弃。

Relations<T>是具有所有可选属性的类型,其键来自T,其值为T的属性中的Relations<DrillDownToEntity<>>。一般来说,大多数属性将是never类型,因为大多数属性本身不能赋值给BaseEntity。观察:

type RelationsProduct = Relations<Product>;
/* type RelationsProduct = {
    name?: undefined;
    description?: undefined;
    categories?: Relations<Category> | undefined;
    hasId?: undefined;
    save?: undefined;
    remove?: undefined;
    softRemove?: undefined;
    recover?: undefined;
    reload?: undefined;
} */
请注意,类型never和类型undefined的可选属性是相同的,至少在未启用the --exactOptionalPropertyTypes compiler flag的情况下是相同的。这会阻止您分配这些类型的任何属性,除非它们undefined。我发现这可能比仅仅省略那些属性要好;根据structural typing{categories?: Relations<Category>}类型的值可能具有也可能没有string值的name属性,而{categories?: Relations<Category>, name?: never}类型的值肯定不会有定义的name属性。

您可以使用Relations的定义验证您的示例代码是否按预期工作。


以下代码:

type Relations<T extends BaseEntity> = {
    [P in keyof T as ExcludeNonEntity<P>]: Relations<T[P]>;
};

不起作用有几个原因,最直接的原因是您正在使用key remapping syntax来假定隐藏不可分配的BaseEntity属性,但是您正在编写ExcludeNonEntity<P>,其中P类型。并且没有密钥将是BaseEntity,因此很可能最终排除所有密钥,即使您可以使其工作。如果要取消按键,则需要选中T[P]而不是P,然后根据该选项省略或包含P。还有其他小问题(例如,属性不是可选的),但最大的问题是将键视为值。

Playground link to code

这篇关于具有映射类型和条件类型的递归类型定义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆