具有映射类型和条件类型的递归类型定义



我正在努力想出一种方法,使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类型的可选属性是相同的,至少在没有启用--exactOptionalPropertyTypes编译器标志的情况下。这将阻止您对这些类型的任何属性进行赋值,除非它们是undefined。我发现这可能比仅仅省略这些属性要好;根据结构类型,{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]>;
};

不工作的原因有几个,最直接的就是你使用关键重新映射语法可能抑制非BaseEntity分配属性,但是你写ExcludeNonEntity<P>P类型的关键。没有将是BaseEntity,所以很可能最终排除所有键,即使你可以让它工作。如果您想抑制键,那么您需要检查T[P]而不是P,然后在此基础上省略或包含P。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。

Playground链接到代码

对jcalz答案的改进:

type Relations<T extends BaseEntity> = {
[K in keyof T as DrillDownToEntity<T[K]> extends never ? never : K]?: Relations<DrillDownToEntity<T[K]>>
}

这样,下面的代码也会导致类型错误,而不会导致jcalz的答案错误:

const b: Relations<Product> = {
name: undefined
}

这个答案的灵感来自安德斯·海尔斯伯格的PR中的以下部分:

as子句中指定的类型解析为never时,不会为该键生成任何属性。因此,as子句可以用作过滤器:

type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
type T60 = Methods<{ foo(): number, bar: boolean }>;  // { foo(): number }

最新更新