首页 > 解决方案 > TS:基于链式方法推断文字类型

问题描述

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

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

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论点,但我不知道如何将其发送回至,实际上我认为这个逻辑是一个烂摊子。 TMixedSchema<T>MixedSchema<T>

我已经研究过 typescript 中的 map 和泛型,但我承认,在将其付诸实践时,并没有什么好的结果。

如果您想尝试,这里是TS 游乐场。

标签: javascripttypescript

解决方案


从概念上讲,您希望required()oneOf()方法缩小T;这很容易提供类型(尽管您需要类型断言来避免编译器错误,因为编译器无法验证您是否确实完成了必要的缩小)。因此required(),在 a上调​​用MixedSchema<T>应该返回 a MixedSchema<Exclude<T, undefined>>(使用实用Exclude程序类型undefined从 的任何联合成员中删除T)。并且在 的元素的元素类型中oneOf()应该是泛型UarrayOfValues,并且应该返回一个MixedSchema<U | Extract<undefined, T>>(如果可以的话,使用Extract实用程序类型来保持)。undefinedTundefined

下面是它的外观(为简洁起见省略了实现):

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使事情变得复杂;你想说,例如, aStringSchema应该StringSchema在调用之后保持某种形式的 a 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中请求的(但未实现)排序的所谓高级类型。您想说:的方法应该返回,您在某种程度上将其视为采用泛型参数的类型。所以如果是 a ,那么它应该是。但是没有直接的支持。MixedSchema<T>required()this<Exclude<T, undefined>>thisthisStringSchema<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 代码链接


推荐阅读