如何在TypeScript中定义可配置的类型防护工厂?

时间:2019-12-19 21:40:11

标签: typescript types

我希望能够“配置”工厂以创建类型保护功能。这样我就可以传入函数(按名称键入),然后为它们创建类型保护程序。 (Playground link

type Success<T> = [true, T]
type Failure = [false, any]
type Result<T> = Success<T> | Failure
type Validator<T> = (x: any) => Result<T>
type Validators = {
  [key: string]: <T>(x: any) => Result<T>
}

type Value<T> = T extends Validator<infer V> ? V : never

const configure = <V extends Validators>(validators: V) => {
  return <K extends keyof V>(key: K) => {
    return <T extends Value<V[K]>>(x: any): x is T => {
      const validator = validators[key]
      const [valid] = validator(x)
      return valid
    }
  }
}

那可以用作...

const factory = configure({
  number: (x: any) => typeof x === 'number' ? [true, x] : [false, 'Invalid!']
  // ^ Error here!
})

const isNumber = factory('number')

const val = 42 as unknown

if (isNumber(val)) {
  val
}

但这会导致此编译错误:

(property) number: (x: any) => [true, number] | [false, string]
Type '(x: any) => [true, number] | [false, string]' is not assignable to type '<T>(x: any) => Result<T>'.
  Type '[true, number] | [false, string]' is not assignable to type 'Result<T>'.
    Type '[true, number]' is not assignable to type 'Result<T>'.
      Type '[true, number]' is not assignable to type 'Success<T>'.
        Type 'number' is not assignable to type 'T'.
          'number' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'.(2322)
input.ts(6, 3): The expected type comes from this index signature.

为什么不能动态识别类型保护的值?

TypeScript Playground link here!

1 个答案:

答案 0 :(得分:2)

您有一些通用的函数类型。这是主要问题:

type Validators = {
  [key: string]: <T>(x: any) => Result<T>
}

该对象的每个属性值的类型均为<T>(x: any)=>Result<T>,这是一个通用函数,声称可以为调用者指定的任何Result<T>生成T < / em>。很难想象这样的函数:如果您有一个名为f的值和一个未知值名为x的值,则可以将其称为f<string>(x)来查看x是否是一个string,然后将其称为f<number>(x)来查看x是否是number,这很令人惊奇,因为f只有一个运行时实现。

因此,编译器正确地抱怨您的Validator<number>不是<T>(x: any)=>Result<T>。真的,什么都不会。

但是,那不是您想要Validators的属性。您真的想说每个属性都是一些特定的 Validator<T>,但是您还不知道哪个。从概念上讲,这称为existential type,但是TypeScript并不容易支持它们。您可以尝试将其表示为映射的对象类型,但是现在我们将举起手来将<T>(x: any)=>Result<T>更改为Validator<any>。至少有可能实现。

现在我们有了

const configure = <V extends {[k: string]: Validator<any>>(validators: V) => {...}

这很好。我通常更喜欢将索引签名替换为如下所示的自引用映射类型:

const configure = <V extends Record<keyof V, Validator<any>>>(validators: V) => {...}

因为它不需要validators具有索引签名,如果它是对象文字或类型别名类型,则可以使用,但fail if it of an interface type可以。


继续前进:在您的实现中,您拥有类型

<T extends Value<V[K]>>(x: any): x is T

又以无法实现的方式通用。如果Value<V[K]>number,则此函数为<T extends number>(x: any): x is T。使用这种类型的名为g的函数和名为x的值,您可以调用g<123>(x)来确定x是否为123,也可以致电g<456>(x)来确定x是否为456。无法实现。让我们将其更改为

(x: any): x is Value<V[K]>

这是您真正想要的:如果Value<V[K]>number,则会得到(x: any): x is number,这是您可以实际实现的非泛型类型保护函数。


因此,我们将所有内容放在一起进行尝试。编译器错误消失,并且:

const factory = configure({
  number: (x: any) => typeof x === 'number' ? [true, x] : [false, 'Invalid!'], // okay
})

const isNumber = factory('number');
//const isNumber: (x: any) => x is string | number ?!

哦,亲爱的。出于某种原因,编译器已推断Value<(x: any) => [true, number] | [false, string]>string | number。这种推论并非完全错误,但并不如应有的精确。此处的解决方案是放弃编译器的神奇条件推断Validator<infer V>,卷起袖子,并尝试从V手动计算T extends Validator<any>

type Value<T extends Validator<any>> = Extract<ReturnType<T>, [true, any]>[1]

那是说:给定T的{​​{1}},首先得到它的返回类型。看起来像Validator<any>。由此,我们应该只看匹配[true, XXX] | [false, YYY] ...的那个联合的那一部分,所以只看[true, any]。最后,获取该元组类型的第二个元素(使用[true, XXX]索引)以获取[1]

现在,最终,发生这种情况:

XXX

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

Playground Link