具有映射和条件类型的递归类型定义
Recursive type definition with mapped and conditional types
我正在尝试想出一种方法来使用 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 enabled. This has the effect of preventing you from assigning any property of these types at all unless they are undefined
. I find this is probably better than merely omitting those properties; a value of type {categories?: Relations<Category>}
might or might not have a string
-valued name
property, according to structural typing,而 {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
是 key 类型。并且没有 keys 会是 BaseEntity
,所以这很可能最终排除所有键,即使你可以让它工作。如果你想禁止按键,那么你需要检查 T[P]
而不是 P
,然后基于此省略或包含 P
。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。
对的改进:
type Relations<T extends BaseEntity> = {
[K in keyof T as DrillDownToEntity<T[K]> extends never ? never : K]?: Relations<DrillDownToEntity<T[K]>>
}
这样,以下内容也会导致类型错误,而 :
不会导致错误
const b: Relations<Product> = {
name: undefined
}
此答案的灵感来自 Anders Hejlsberg's PR 中的以下部分:
When the type specified in an as
clause resolves to never
, no property is generated for that key. Thus, an as
clause can be used as a filter:
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 }
我正在尝试想出一种方法来使用 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 enabled. This has the effect of preventing you from assigning any property of these types at all unless they are undefined
. I find this is probably better than merely omitting those properties; a value of type {categories?: Relations<Category>}
might or might not have a string
-valued name
property, according to structural typing,而 {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
是 key 类型。并且没有 keys 会是 BaseEntity
,所以这很可能最终排除所有键,即使你可以让它工作。如果你想禁止按键,那么你需要检查 T[P]
而不是 P
,然后基于此省略或包含 P
。还有其他一些小问题(例如,属性不是可选的),但最大的问题是将键视为值。
对
type Relations<T extends BaseEntity> = {
[K in keyof T as DrillDownToEntity<T[K]> extends never ? never : K]?: Relations<DrillDownToEntity<T[K]>>
}
这样,以下内容也会导致类型错误,而
const b: Relations<Product> = {
name: undefined
}
此答案的灵感来自 Anders Hejlsberg's PR 中的以下部分:
When the type specified in an
as
clause resolves tonever
, no property is generated for that key. Thus, anas
clause can be used as a filter: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 }