将类型定义委托给实现

时间:2020-03-28 23:30:12

标签: typescript typescript-typings typescript-generics

我要正确键入以下对象。这里的key是接口的键,T是字符串,数字或布尔值,而E是某些键和字符串值的对象。问题是E对于对象的每个条目都是不同的,我想保留每个对象的类型信息。

{
   [key]: (errorMessage?: E) => Validation<T, E>
}

例如,我可能会有类似的东西。

interface StudentSchema {
  name: string;
  id: string;
  country: string;
  age: number;
}

interface Validation<T, E> {
  foo: string;
  bar: number;
  validations: (errorMessages? E) => SchemaValidation<T>;
}

interface SchemaValidation<T> {
   setIsRequired(message?: string) => SchemaValidation<T>;
   setMatches(regex: any, message?: string) => SchemaValidation<T>;
   setMax(max: number, message?: string) => SchemaValidation<T>;
   isValid (data: T) => boolean;
}

class StringSchemaValidation extends SchemaValidation<string> { /* ... */ }
class NumberSchemaValidation extends SchemaValidation<number> { /* ... */ }

const StudentValidations: Record<keyof StudentSchema, Validation<T, E>> = {
  name: {
    foo: 'AAA',
    bar: 1,
    validation: (errorMessages?: { required: string }): SchemaValidation<string> =>
      new SchemaValidation()
      .setIsRequired(errorMessages?.required),
  },
  id: {
    foo: 'BBB',
    bar: 2,
    validation: (errorMessages?: { required: string; match: string; max: (max: number) => string }): SchemaValidation<string> =>
       new SchemaValidation()
      .setIsRequired(errorMessages?.required)
      .setMatches(/[a-zA-Z0-9]+/, errorMessages?.match)
      .setMax(30, errorMessage?.max),
  },
  country: {
    foo: 'CCC',
    bar: 3,
    validation: (errorMessages?: { required: string }): SchemaValidation<string> =>
      new SchemaValidation()
      .setIsRequired(errorMessage?.required),
  },
  age: {
    foo: 'DDD',
    bar: 4,
    validation: (errorMessages?: { required: string }): SchemaValidation<number> =>
      new NumberSchemaValidation()
      .setIsRequired(errorMessage?.required),
  },
};

首先,这不会编译,因为我没有为E指定StudentValidations,但是我不知道要放什么,因为每个条目的内容都不相同。我可以使用any或类似的东西,但是当我访问validations函数时,我会丢失类型信息。

我想要的实际上是将类型声明“委托”给实现,但这似乎是不可能的。有没有办法实现我想要的?或者,也许有另一种方法可以实现这一目标?

1 个答案:

答案 0 :(得分:0)

我认为这里有两个问题。在从Validation<T, E>之类的接口派生的StudentSchema中,它具有与StudentSchema相同的键,对于每个键KT类型是相应的属性StudentSchema[K]。这部分是mapped types的完美用例。看起来像这样:

/* type ValidationFor<T> = { [K in keyof T]: Validation<T[K], ...>}; */
/* type ValidationForStudentSchema = ValidationFor<StudentSchema>; */

其中...是我们要为E做的未知的事情。您可以在此处执行的操作是提供另一个对象类型E,该对象类型具有与StudentSchema相同的键,其属性用于插入{中的E {1}}。看起来像这样:

Validation

然后您的示例type ValidationFor<T, E extends Record<keyof T, any>> = { [ K in keyof T ]: Validation<T[K], E[K]> }; type ValidationForStudentSchema<E extends Record<keyof StudentSchema, any>> = ValidationFor<StudentSchema, E> */ 将具有以下类型:

StudentValidations

但是这要么需要我们明确写出const StudentValidations: ValidationFor<StudentSchema, { name: { required: string }, country: { required: string; }, age: { required: string; }, id: { required: string; match: string; max: (max: number) => string; } }> = ...; 对象类型,要么尝试让编译器进行推断,这两种方法都不是一件特别有趣的事情。


相反,我建议我们放宽对E属性的约束,并故意使用any类型的“一切”:

E

那行得通; /* type ValidationFor<T> = { [K in keyof T]: Validation<T[K], any> }; */ 会与您想要的值匹配,但这太“让人忘记”了;因为它将使用ValidationFor<StudentSchema>而不是您提供的实际any属性。为了使编译器记忆犹新,我们制作了一个generic辅助函数,该函数仅采用E的通用类型的值:

extends ValidationFor<StudentSchema>

如果您将与const valsForStudentSchema = <U extends { [K in keyof StudentSchema]: Validation<StudentSchema[K], any> }>( valMap: U ) => valMap; 不匹配的参数传递给valsForStudentSchema(),则会收到错误消息。如果您传递匹配的参数 ,则编译器将返回该参数而不会将其扩展为ValidationFor<StudentSchema>并且会忘记。

赞:

ValidationFor<StudentSchema>

请注意,您询问是否要与其他模式(不仅仅是const StudentValidations = valsForStudentSchema({ name: { foo: 'AAA', bar: 1, validation: (errorMessages?: { required: string }): SchemaValidation<string> => new StringSchemaValidation() .setIsRequired(errorMessages?.required), }, id: { foo: 'BBB', bar: 2, validation: (errorMessages?: { required: string; match: string; max: (max: number) => string }): SchemaValidation<string> => new StringSchemaValidation() .setIsRequired(errorMessages?.required) .setMatches(/[a-zA-Z0-9]+/, errorMessages?.match) .setMax(30, errorMessages?.max(30)), }, country: { foo: 'CCC', bar: 3, validation: (errorMessages?: { required: string }): SchemaValidation<string> => new StringSchemaValidation() .setIsRequired(errorMessages?.required), }, age: { foo: 'DDD', bar: 4, validation: (errorMessages?: { required: string }): SchemaValidation<number> => new NumberSchemaValidation() .setIsRequired(errorMessages?.required), }, }); )一起使用。我们可以使用currying来做到这一点,方法是创建一个采用模式类型的泛型函数,并返回另一个生成StudentSchema该类型的泛型函数:

ValidationFor

您可以将其与其他类似类型的产品一起使用:

const validationFor = <T>() => <U extends { [K in keyof T]: Validation<T[K], any> }>(
    valMap: U
) => valMap;

const StudentValidations = validationFor<StudentSchema>()({
    // ... put the same object here as before
});

好的,希望能有所帮助;祝你好运!

Playground link to code