我只剩下一件事来解锁我的流程并发布我的表单验证库的第一个试用版本。
我有以下代码(当然我省略了很多东西,所以它不会变得太大)
interface Validation {
name: string
message: string
params?: Record<string, any>
test: (value: any, params?: any) => boolean
}
class MixedSchema<T> {
type!: T
validations: Validation[] = []
required(message?: string) {
this.validations.push({
name: 'required',
message: message,
test: (value: any) => {
return value === '' ? false : true
},
})
return this
}
oneOf(arrayOfValues: any[], message?: string) {
type Params = { arrayOfValues: any[] }
this.validations.push({
name: 'oneOf',
params: { arrayOfValues },
message: message,
test: (value: any, params: Params) => {
return params.arrayOfValues.includes(value)
},
})
return this
}
}
class StringSchema extends MixedSchema<string> {
email() {
// this.validations.push({ ... })
return this
}
maxWords(maxWords: number, message?: string) {
// this.validations.push({ ... })
return this
}
}
class NumberSchema extends MixedSchema<number> {
min(min: number, message?: string) {
// this.validations.push({ ... })
return this
}
round(type: 'toUp' | 'toDown' | 'closer') {
// this.validations.push({ ... })
return this
}
}
const schema = {
string() {
return new StringSchema()
},
number() {
return new NumberSchema()
},
// array, file, etc..
}
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
type InferType<T> = {
[P in keyof T]: T[P] extends MixedSchema<infer TS> ? TS : never
}
type Form = InferType<typeof form>
所以我得到以下结果
但我需要获得尽可能真实的输入,我的意思是,类似于 form
定义的模式,示例
interface HowNeedsToBe {
email: string
age: number | undefined
gender: 'male' | 'female'
color: 'red' | 'blue' | 'green' | undefined
}
我相信逻辑是这样的,在没有 required
的情况下,输入 undefined
如果有oneOf
,将参数替换为 T
MixedSchema<T>
的,但我不知道如何将其发送回MixedSchema<T>
,其实我觉得这个逻辑很乱。
我已经研究过 typescript 中的映射和泛型,但我承认,当将其付诸实践时,并没有什么好的结果。
这是TS playground如果你想尝试的话。
最佳答案
从概念上讲,您需要 required()
和oneOf()
缩小范围的方法T
;这很容易提供类型(尽管您需要 type assertions 以避免编译器错误,因为编译器无法验证您是否确实完成了必要的缩小)。所以required()
,调用MixedSchema<T>
,应该返回 MixedSchema<Exclude<T, undefined>>
(使用 the Exclude
utility type 从 undefined
的任何联合成员中删除 T
)。和oneOf()
应该是generic在元素类型 U
中arrayOfValues
的元素,并且应该返回 MixedSchema<U | Extract<undefined, T>>
(如果 Extract
可以是 undefined
,则使用 the T
utility type 保留 undefined
)。
它的外观如下(为简洁起见,省略了实现):
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): MixedSchema<Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
MixedSchema<U | Extract<undefined, T>>
}
不幸的是,您正在子类化 MixedSchema
使事情变得复杂化;例如,您想说 StringSchema
应该留下StringSchema
调用 required()
后的某种信息;它不应该扩大回 MixedSchema
:
declare class StringSchema<T extends string | undefined> extends MixedSchema<T> {
email(): this;
maxWords(maxWords: number, message?: string): this;
}
declare class NumberSchema<T extends number | undefined> extends MixedSchema<T> {
min(min: number, message?: string): this;
round(type: 'toUp' | 'toDown' | 'closer'): this;
}
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // MixedSchema<string>
t.email(); // error! Property 'email' does not exist on type 'MixedSchema<string>';
所以我们需要更复杂的东西。
这里的“正确”答案是使用 microsoft/TypeScript#1213 中请求(但未实现)的所谓的更高类型。 。您想说类似:MixedSchema<T>
的required()
方法应返回 this<Exclude<T, undefined>>
,你正在以某种方式治疗this
就像采用泛型参数的类型一样。所以如果 this
是 StringSchema<T>
,那么应该是StringSchema<Exclude<T, undefined>>
。但对此没有直接支持。
相反,我们需要模拟它,所有此类模拟都将涉及一定量的“注册”我们希望能够像泛型中的泛型一样对待的类型:
type Specify<C extends MixedSchema<any>, T> =
C extends NumberSchema<any> ? NumberSchema<Extract<T, number | undefined>> :
C extends StringSchema<any> ? StringSchema<Extract<T, string | undefined>> :
MixedSchema<T>;
我们列出了 MixedSchema
的所有子类我们关心并描述了如何指定它们的类型参数。所以虽然我们不能写 this<Exclude<T, undefined>>
,但我们可以写 Specify<this, Exclude<T, undefined>>
并具有相同的效果。
这是 MixedSchema
的新实现:
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): Specify<this, Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
Specify<this, U | Extract<undefined, T>>
}
我们可以验证它现在在子类中的行为是否正确:
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // StringSchema<string>
t.email(); // okay
让我们确保类型是按照您的意愿推断的:
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
/* const form: {
email: StringSchema<string>;
age: NumberSchema<number | undefined>;
gender: StringSchema<"male" | "female">;
color: StringSchema<"red" | "blue" | "green" | undefined>;
} */
这是一个好兆头;每个字段的通用类型参数都已以正确的方式指定。因此你的InferType
应该能够获取这些字段类型:
type Form = InferType<typeof form>
/* type Form = {
email: string;
age: number | undefined;
gender: "male" | "female";
color: "red" | "blue" | "green" | undefined;
} */
看起来不错!
关于javascript - TS : Infer literal typing based on chained methods,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67627364/