【问题标题】:Delegate type definitions to implementation将类型定义委托给实现
【发布时间】:2020-03-30 05:01:35
【问题描述】:

我想正确键入以下对象。这里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),
  },
};

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

我想要的本质上是将类型声明“委托”给实现,但这似乎是不可能的。有没有办法实现我想要的?或者,也许有不同的方法来实现这一点?

【问题讨论】:

  • 您能否修改此代码以构成不依赖于“yup”的minimal reproducible example,除非您的问题专门针对该库?
  • 也许this 适合你?
  • @jcalz 您可以完全忽略/替换 yup。它是什么并不重要。你提供的那个例子有效!但是,我希望能够将不同的接口传递给 ssVals。目前,它被硬编码为 StudentSchema ......是否可以将其推广到其他人?我也想不通。 (这对我来说有点像魔术)。
  • 喜欢this?
  • 如果您将示例更改为没有明显库依赖性的可回答问题,我很乐意将其写成答案。否则我的答案的前半部分将是我自己设置问题代码,以便我可以提供回答它的代码。

标签: typescript typescript-typings typescript-generics


【解决方案1】:

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

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

... 是我们想要为E 做的未知的事情。您可以在这里做的是提出另一个对象类型E,它与StudentSchema 具有相同的键,其属性用于插入Validation 中的E。它看起来像:

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 对象类型,或者尝试让编译器推断它,这两者都不是特别有趣。


相反,我建议我们放宽对E 属性的约束,并有意使用any 的“anything-goes”类型:

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

这行得通; ValidationFor&lt;StudentSchema&gt; 会匹配你想要的值,但它太“健忘”了;因为它将使用 any 而不是您给它的实际 E 属性。为了让编译器记住,我们创建了一个generic 辅助函数,它只接受extends ValidationFor&lt;StudentSchema&gt; 的泛型类型的值:

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

如果您将与ValidationFor&lt;StudentSchema&gt; 不匹配的参数传递给valsForStudentSchema(),则会收到错误消息。如果您传递一个 确实 匹配的参数,编译器只会返回它,而不会将其扩大到 ValidationFor&lt;StudentSchema&gt; 并忘记。

像这样:

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),
    },
});

请注意,您询问了与其他模式一起使用的东西,而不仅仅是StudentSchema。我们可以使用currying 做到这一点,方法是创建一个通用函数,该函数采用模式类型并返回另一个生成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
});

您可以将它与其他类型一起使用,如下所示:

interface Foo {
    a: boolean
}

const foo = validationFor<Foo>()({
    a: {
        foo: "",
        bar: 1,
        validation: (e?: { a: string }): SchemaValidation<boolean> =>
            new BooleanSchemaValidation()
    }
})

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

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-23
    • 2018-05-07
    • 1970-01-01
    • 1970-01-01
    • 2011-12-16
    • 1970-01-01
    相关资源
    最近更新 更多