javascript - TS : Infer literal typing based on chained methods

标签 javascript typescript

我只剩下一件事来解锁我的流程并发布我的表单验证库的第一个试用版本。

我有以下代码(当然我省略了很多东西,所以它不会变得太大)

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>

所以我得到以下结果

enter image description here

但我需要获得尽可能真实的输入,我的意思是,类似于 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 typeundefined 的任何联合成员中删除 T )。和oneOf()应该是generic在元素类型 UarrayOfValues 的元素,并且应该返回 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就像采用泛型参数的类型一样。所以如果 thisStringSchema<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;
} */

看起来不错!

Playground link to code

关于javascript - TS : Infer literal typing based on chained methods,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67627364/

相关文章:

javascript - 如何使用 Javascript 编辑 JSON?

javascript - 递归 |两个函数名

angular - promise 在我解决它并更改页面时继续工作( Protractor 测试)

angularjs - 拒绝从 '*.ts' 执行脚本,因为它的 MIME 类型 ('video/vnd.dlna.mpeg-tts' ) 不可执行

typescript - Angular 2 : How to pass route parameters to subroute?

javascript - TypeScript(JavaScript) 中的项目范围

javascript - 在 Yii 上使用 JavaScript 进行验证

javascript - coffeescript 类中的实例变量和常量

javascript - 使用for循环在html中包含多个脚本文件

node.js - Nodejs 中的中间件管道