如何推断 TypeScript 中的可选属性?
How can I infer optional properties in TypeScript?
我有以下数据库模式的类型定义:
type Schema<T> = {
[K in keyof T]: SchemaType<T[K]>
}
interface SchemaType<T> {
optional: boolean
validate(t: T): boolean
}
这是一个实现它的对象:
const userSchema = {
name: {
optional: true,
validate(name: string) {
return name.length > 0 && name.length < 30
}
}
}
我希望能够根据 userSchema
对象推断 Schema
中的泛型类型 T
。使用 TypeScript 的 infer
关键字无需太多努力就可以做到这一点:
type extractType<T> = T extends Schema<infer X> ? X : null
// User is inferred as { name: string }
type User = extractType<typeof userSchema>
更好的是,我能够间接推断出 User
类型:
class Model<T> {
constructor(private schema: Schema<T>) {}
}
// userModel is inferred as type Model<{name: string}>
const userModel = new Model(userSchema)
但是,我希望能够将 User
推断为 { name?: String }
,其中 name
是可选的,因为它的 optional
属性 设置为真的。有办法吗?
一些相关的 TypeScript 功能
这是我在处理此问题时 运行 遇到的一些可能有用的 TypeScript 功能。
TypeScript 有discriminated unions,这与我试图实现的类型推断类似。
虽然上面 userSchema
上的 optional
字段被推断为 boolean
,但可以使用 [=28= 将其推断为布尔文字].
// userSchema is inferred as
// { name: { optional: true, validate(name: string): boolean } }
// instead of
// { name: { optional: boolean, validate(name: string): boolean } }
const userSchema = {
name: {
optional: true as const,
validate(name: string) {
return name.length > 0 && name.length < 30
}
}
}
要求这样做并不理想,但我不确定它是否可以避免。
我尝试像上面的 extractType
实现中那样使用 conditional types 以更有限的方式定义 Schema
:
// The below interface essentially says:
// if T[K] is an optional property
// then -> optional is set to true
// else -> optional is set to false.
// I was hoping TypeScript could reverse this when inferring the type:
// if optional is set to true
// then -> T[K] is an optional property
// else -> T[K] is a required property
type Schema<T> = {
[K in keyof T]: undefined extends T[K]
? OptionalSchemaType<T[K]> // Same as SchemaType but with optional: true
: RequiredSchemaType<T[K]> // Same as SchemaType but with optional: false
}
const userModel = new Model(userSchema) // Error!
不幸的是,TypeScript 的类型推断不够智能,无法推断出如此复杂的类型。
我不确定我想要实现的目标是否可行,但非常感谢任何帮助或替代方案。 :)
首先,您可能需要编写 true as const
或 true as true
或类似的东西,因为编译器会倾向于将布尔文字扩展为 boolean
类型,除非它们在编译器期望更窄类型的上下文。避免显式转换的一种可能方法是使用一个辅助函数,该函数 returns 它的输入期望在这样一个更窄类型的上下文中:
const asSchema = <S extends Record<keyof S, { optional: B }>, B extends boolean>(
s: S
) => s;
const userSchema = asSchema({
name: {
optional: true,
validate(name: string) {
return name.length > 0 && name.length < 30
}
},
age: {
optional: false,
validate(age: number) {
return age >= 0 && age < 200;
}
}
});
你可以验证userSchema
的类型是:
/* const userSchema: {
name: {
optional: true;
validate(name: string): boolean;
};
age: {
optional: false;
validate(age: number): boolean;
};
} */
如果您打算创建很多这样的东西,那么使用辅助函数可能是值得的;或者你可以只写 optional: false as false
。无论如何,继续前进:
我认为您需要直接计算类型而不是依赖类型推断。直接方法需要大量明确地写出类型的哪些部分会变成什么。让我们提出一些实用程序类型:
PartialPartial<T, K>
类型采用一个类型 T
及其一组键 K
和 returns 一个类似于 T
的新类型,除了所有的K
索引的属性是可选的。所以,PartialPartial<{a: string, b: number}, "a">
是 {a?: string, b: number}
.
type PartialPartial<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K> extends
infer O ? { [P in keyof O]: O[P] } : never;
KeysMatching<T, V>
类型采用类型 T
和 returns T
的所有键,其属性可分配给 V
。所以,KeysMatching<{a: string, b: number}, number>
是 "b"
.
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
现在 ExtractType<T>
。给定一个类似模式的 T
,首先映射它的每个属性并将第一个参数拉出到 validate()
方法。然后,取整个结果,并将 optional
属性 为 true
:
的所有属性设为可选
type ExtractType<T extends { [K in keyof T]: SchemaType<any> }> = PartialPartial<{
[K in keyof T]: Parameters<T[K]['validate']>[0]
}, KeysMatching<T, { optional: true }>>
在上面的 userSchema
上进行测试:
type UserThing = ExtractType<typeof userSchema>;
/* type UserThing = {
name?: string | undefined;
age: number;
} */
看起来不错。
如果我们放弃类型推断,这意味着您不能轻易地让 Model<T>
由 Schema<T>
构造(至少不能使用标准 class
语句)。解决这个问题的一种方法是让 Model
在模式类型 S
中是通用的,并在任何您想使用 T
的地方使用 ExtractType<S>
。您甚至可以将 T
放在类型定义中,因此您有 Model<S, T>
,其中 S
是架构类型,T
是提取的类型:
class Model<S extends Record<keyof S, SchemaType<any>>, T extends ExtractType<S> = ExtractType<S>> {
constructor(private schema: S) { }
}
const userModel = new Model(userSchema);
// S is typeof userSchema
// T is {name?: string, age: number}
我对标准 class
语句的警告:您可以决定通过 ModelConstructor
接口来描述 Model
的静态方面,该接口允许您指定与constructor
方法和实例类型之间通常可用的是什么:
interface MyModelConstructor {
new <S extends Record<keyof S, SchemaType<any>>>(schema: S): MyModel<ExtractType<S>>;
}
interface MyModel<T> {
something: T;
}
const MyModel = Model as any as MyModelConstructor;
并测试它:
const myUserModel = new MyModel(userSchema);
/* const myUserModel: MyModel<{
name?: string | undefined;
age: number;
}> */
在这里你可以看到 new MyModel(userSchema)
产生了 MyModel<{name?: string, age: number}>
。这很好,但是您会发现自己必须为 class 的 static/instance 端写出冗余接口声明,然后将另一个 class 构造函数分配给它。复制可能不值得;由你决定。
好的,我知道有很多东西。希望对你有帮助。祝你好运!
我有以下数据库模式的类型定义:
type Schema<T> = {
[K in keyof T]: SchemaType<T[K]>
}
interface SchemaType<T> {
optional: boolean
validate(t: T): boolean
}
这是一个实现它的对象:
const userSchema = {
name: {
optional: true,
validate(name: string) {
return name.length > 0 && name.length < 30
}
}
}
我希望能够根据 userSchema
对象推断 Schema
中的泛型类型 T
。使用 TypeScript 的 infer
关键字无需太多努力就可以做到这一点:
type extractType<T> = T extends Schema<infer X> ? X : null
// User is inferred as { name: string }
type User = extractType<typeof userSchema>
更好的是,我能够间接推断出 User
类型:
class Model<T> {
constructor(private schema: Schema<T>) {}
}
// userModel is inferred as type Model<{name: string}>
const userModel = new Model(userSchema)
但是,我希望能够将 User
推断为 { name?: String }
,其中 name
是可选的,因为它的 optional
属性 设置为真的。有办法吗?
一些相关的 TypeScript 功能
这是我在处理此问题时 运行 遇到的一些可能有用的 TypeScript 功能。
TypeScript 有discriminated unions,这与我试图实现的类型推断类似。
虽然上面
userSchema
上的optional
字段被推断为boolean
,但可以使用 [=28= 将其推断为布尔文字].// userSchema is inferred as // { name: { optional: true, validate(name: string): boolean } } // instead of // { name: { optional: boolean, validate(name: string): boolean } } const userSchema = { name: { optional: true as const, validate(name: string) { return name.length > 0 && name.length < 30 } } }
要求这样做并不理想,但我不确定它是否可以避免。
我尝试像上面的
extractType
实现中那样使用 conditional types 以更有限的方式定义Schema
:// The below interface essentially says: // if T[K] is an optional property // then -> optional is set to true // else -> optional is set to false. // I was hoping TypeScript could reverse this when inferring the type: // if optional is set to true // then -> T[K] is an optional property // else -> T[K] is a required property type Schema<T> = { [K in keyof T]: undefined extends T[K] ? OptionalSchemaType<T[K]> // Same as SchemaType but with optional: true : RequiredSchemaType<T[K]> // Same as SchemaType but with optional: false } const userModel = new Model(userSchema) // Error!
不幸的是,TypeScript 的类型推断不够智能,无法推断出如此复杂的类型。
我不确定我想要实现的目标是否可行,但非常感谢任何帮助或替代方案。 :)
首先,您可能需要编写 true as const
或 true as true
或类似的东西,因为编译器会倾向于将布尔文字扩展为 boolean
类型,除非它们在编译器期望更窄类型的上下文。避免显式转换的一种可能方法是使用一个辅助函数,该函数 returns 它的输入期望在这样一个更窄类型的上下文中:
const asSchema = <S extends Record<keyof S, { optional: B }>, B extends boolean>(
s: S
) => s;
const userSchema = asSchema({
name: {
optional: true,
validate(name: string) {
return name.length > 0 && name.length < 30
}
},
age: {
optional: false,
validate(age: number) {
return age >= 0 && age < 200;
}
}
});
你可以验证userSchema
的类型是:
/* const userSchema: {
name: {
optional: true;
validate(name: string): boolean;
};
age: {
optional: false;
validate(age: number): boolean;
};
} */
如果您打算创建很多这样的东西,那么使用辅助函数可能是值得的;或者你可以只写 optional: false as false
。无论如何,继续前进:
我认为您需要直接计算类型而不是依赖类型推断。直接方法需要大量明确地写出类型的哪些部分会变成什么。让我们提出一些实用程序类型:
PartialPartial<T, K>
类型采用一个类型 T
及其一组键 K
和 returns 一个类似于 T
的新类型,除了所有的K
索引的属性是可选的。所以,PartialPartial<{a: string, b: number}, "a">
是 {a?: string, b: number}
.
type PartialPartial<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K> extends
infer O ? { [P in keyof O]: O[P] } : never;
KeysMatching<T, V>
类型采用类型 T
和 returns T
的所有键,其属性可分配给 V
。所以,KeysMatching<{a: string, b: number}, number>
是 "b"
.
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
现在 ExtractType<T>
。给定一个类似模式的 T
,首先映射它的每个属性并将第一个参数拉出到 validate()
方法。然后,取整个结果,并将 optional
属性 为 true
:
type ExtractType<T extends { [K in keyof T]: SchemaType<any> }> = PartialPartial<{
[K in keyof T]: Parameters<T[K]['validate']>[0]
}, KeysMatching<T, { optional: true }>>
在上面的 userSchema
上进行测试:
type UserThing = ExtractType<typeof userSchema>;
/* type UserThing = {
name?: string | undefined;
age: number;
} */
看起来不错。
如果我们放弃类型推断,这意味着您不能轻易地让 Model<T>
由 Schema<T>
构造(至少不能使用标准 class
语句)。解决这个问题的一种方法是让 Model
在模式类型 S
中是通用的,并在任何您想使用 T
的地方使用 ExtractType<S>
。您甚至可以将 T
放在类型定义中,因此您有 Model<S, T>
,其中 S
是架构类型,T
是提取的类型:
class Model<S extends Record<keyof S, SchemaType<any>>, T extends ExtractType<S> = ExtractType<S>> {
constructor(private schema: S) { }
}
const userModel = new Model(userSchema);
// S is typeof userSchema
// T is {name?: string, age: number}
我对标准 class
语句的警告:您可以决定通过 ModelConstructor
接口来描述 Model
的静态方面,该接口允许您指定与constructor
方法和实例类型之间通常可用的是什么:
interface MyModelConstructor {
new <S extends Record<keyof S, SchemaType<any>>>(schema: S): MyModel<ExtractType<S>>;
}
interface MyModel<T> {
something: T;
}
const MyModel = Model as any as MyModelConstructor;
并测试它:
const myUserModel = new MyModel(userSchema);
/* const myUserModel: MyModel<{
name?: string | undefined;
age: number;
}> */
在这里你可以看到 new MyModel(userSchema)
产生了 MyModel<{name?: string, age: number}>
。这很好,但是您会发现自己必须为 class 的 static/instance 端写出冗余接口声明,然后将另一个 class 构造函数分配给它。复制可能不值得;由你决定。
好的,我知道有很多东西。希望对你有帮助。祝你好运!