typescript - 具有映射和条件类型的递归类型定义
问题描述
我正在尝试想出一种方法来使用 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]>> }
TheDrillDownToEntity<T>
类似于您的Flatten<T>
类型与 混合ExcludeNonEntity<T>
,除了它以递归方式运行。它为任意数量的嵌套提取所有数组元素类型,只保留那些可分配给BaseEntity
. 观察:
type DrillTest = DrillDownToEntity<Category | string | Product[] | Supplier[][][][][]>
// type DrillTest = Category | Product | Supplier
我不知道您是否会拥有数组数组;如果你不是,你可以使它成为非递归的。但重要的是,任何最终不能分配给的类型都将BaseEntity
被丢弃。
ThenRelations<T>
是具有所有可选属性的类型,其键来自T
,其值Relations<DrillDownToEntity<>>
来自 的属性T
。一般来说,大多数属性都属于 类型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
,至少没有启用编译器标志。这具有阻止您分配这些类型的任何属性的效果,除非它们是. 我发现这可能比仅仅省略这些属性要好;根据结构类型,类型的值可能有也可能没有-valued属性,而其中一种形式肯定根本没有定义的属性。--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>
whereP
是键类型。并且不会有任何键,BaseEntity
因此很可能最终会排除所有键,即使您可以使其正常工作。如果要禁止键,则需要检查T[P]
and not P
,然后P
基于此省略或包含。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。
推荐阅读
- swift - SwiftUI 下 TabView 是如何实现的
- jquery - jQuery/Thymeleaf - 设置选中的值
- javascript - Beautifulsoup 抓取 javascript 表问题 csv
- python - 使用 Seaborn FacetGrid 时的 Figuresize
- angular - 使用哪种技术来更新视口中的数千个元素(如 flightradar24.com)并具有良好的性能?
- xamarin - 如何在 Xamarin 中为相同的自定义字体添加多个字体文件
- sql - 许多表之间的桥接表
- spring-boot - 为 Spring Boot 应用程序启用 ssl 在 Windows 上工作但在 centos 7 上失败
- javascript - fullcalendar 谷歌隐藏过去的事件
- javascript - 如何在 React 中控制音频/视频 DOM 元素