typescript 如何强制一个对象的一些嵌套键是某种类型的?

7rtdyuoh  于 2023-01-18  发布在  TypeScript
关注(0)|答案(2)|浏览(125)

我正在寻找一个类型Validate,它将强制执行类似以下内容

// validate
const myObj: Validate<'value', boolean> = {
   value: true,
   otherValue: 'its value',
   nestedProp: {
       value: false
   }
}

// does not validate
const myObj: Validate<'value', boolean> = {
   value: true,
   otherValue: 'its value',
   nestedProp: {
       value: 0 // not of type boolean
   }
}

其中泛型的第一个参数是嵌套属性的名称,第二个参数是要强制的类型。
这可能吗?
编辑:其思想是在使用联合类型而不是该示例中的boolean时从IDE中获得自动完成。

type ColorType = 'red' | 'green'

type ColoredObject<T> = {
  [P in keyof T]: T[P] extends object ? ColoredObject<T[P]> : T[P];
} & { color: ColorType };

const myObject: ColoredObject<{ prop1: { prop2: { color: string } } }> = {
  color: 'red',
  prop1: {
    color: 'red',
    prop2: {
      color: 0 // this would fail
    }
  }
};

在上面的例子中,它按照我想要的方式工作,但是它使color属性成为必需的。有没有一种方法可以使它成为可选的而不违反要求?同样,这要求我将对象接口作为泛型传递,我不希望这样,因为我可能不知道它。
编辑2:或者这个,但是除了颜色,它不允许任何其他的 prop

type ColoredObject<T, P, V> = {
  [P in keyof T]: T[P] extends object ? ColoredObject<T[P], P, V> : T[P] extends 'color' ? V : never
}

type ColorType = 'red' | 'green'
const test: ColoredObject<any, 'color', ColorType> = {
  color: 'green',
  otherProps: 'lala', // <---- fails here
  props: {
    color: 'green'
  }
}
qxgroojn

qxgroojn1#

给定一个键类型K和一个值类型V,是否存在一个特定的类型Validate<K, V>来执行您希望执行的规则并不明显,通常当很难或不可能编写对应于某个规则的特定类型时,编写 * 检查 * 候选类型是否遵守该规则的generic类型变得简单得多。
因此,您使用T extends Validate<T, K, V>而不是Validate<K, V>作为自引用类型 constraint。为了避免手动指定T类型,您可以编写一个通用帮助器函数来为您 * infert * 它。
因此,从概念上讲,该方法是:而不是const x: Validate<K, V> = ...,您可以编写以下形式的函数

declare const validateKV = <T extends Validate<T, K, V>>(t: T) => t;

然后写const x = validateKV(...);。当然,这并不总是那么容易;T extends Validate<T, K, V>通常被认为是非法的循环约束,要解决这个问题,您需要使用conditional types,例如

declare const validateKV = <T,>(
  t: T extends Validate<T, K, V> ? T : Validate<T, K, V>
) => t;

你可能不想为每一个可能的KV写一个新的helper函数,所以它也应该是泛型的,不幸的是,你不能得到“部分推理”,在这里你指定KV,但是编译器推理V(参见microsoft/TypeScript#26242了解特性请求),因此您需要解决这个问题(参见Typescript: infer type of generic after optional first generic了解更多信息),最简单的方法是 currying,如下所示:

const validate = <K extends PropertyKey, V>(k: K, v: V) =>
  <T,>(t: T extends Validate<T, K, V> ? T : Validate<T, K, V>) => t;

一旦我们定义了Validate<T, K, V>,这就是我们要用到的。
这就是:

type Validate<T, K extends PropertyKey, V> =
  T extends object ? {
    [P in keyof T]: P extends K ? V : Validate<T[P], K, V>
  } : T;

其思想是,如果T是一个基元类型,那么Validate<T, K, V>就是T,因此Validate<T, K, V>将自动接受任何基元类型,另一方面,如果它是一个对象类型,则Validate<T, K, V>mapped type,其中检查每个属性键P。如果属性键P碰巧与特殊键K相同,那么我们需要确保它具有值类型V,否则,我们只需要向下递归并确保属性类型T[P]对于相同的KV可赋值给Validate<T[P], K, V>
让我们来测试一下,对于"value"的特定Kboolean的特定V,如问题代码中所提到的。首先,我们需要使用curried outer函数:

const validateValueBoolean = validate("value", Math.random() < 0.5);

现在我们可以试试。

const myObj = validateValueBoolean({
  value: true,
  otherValue: 'its value',
  nestedProp: {
    value: false,
    nested: {
      value: true
    }
  },
  me: {
    value: true,
    other: 23,
    somethingElse: "",
    whoKnows: {
      foo: {
        value: true
      },
      value: false
    }
  }
}); // okay

该类型根据需要进行检查。在每个级别上,名为value的属性都具有boolean类型的值。请将其与以下内容进行对比:

const myObj2 = validateValueBoolean({
  value: true,
  otherValue: 'its value',
  nestedProp: {
    value: false,
    nested: {
      value: true
    }
  },
  me: {
    value: true,
    other: 23,
    somethingElse: "",
    whoKnows: {
      foo: {
        value: 3 // error, number is not a boolean
      },
      value: false
    }
  }
});

这是一个编译器错误,特别是错误的value属性,它告诉你3是一个number,而预期的是boolean。如果你修复了这个错误,那么这个错误就会消失。
所以,看起来不错。考虑到语言的限制和我对用例的理解,这是我所能想象的最接近你想要的类型。
Playground代码链接

2lpgd968

2lpgd9682#

这个怎么样?

type Validate<K extends string, T> = {
  value: boolean,
  otherValue: string,
  nestedProp:  {
    [key in K] :T
  }
}

感谢Titian的评论

type Validate<K extends string, T> = {
      value: boolean,
      otherValue: string,
      nestedProp: Record<K, T>
    }

相关问题