【问题标题】:Typescript loop typed object打字稿循环类型对象
【发布时间】:2020-06-10 00:37:41
【问题描述】:

我有这个验证文件

interface Rule {
  required?: true;
  isNumber?: true;
  isDate?: {
    format: string;
  }
// ... some other rules
// ruleNames: RuleOptions
}

const RuleMap: {
  [key in keyof Required<Rule>]: (
    value: any,
    options: Required<Rule>[key]
  ) => boolean;
} = {
  required: (value) => value != null,
  isNumber: (value) => isNumber(value),
  isDate: (value, options) => isDate(value, options),
//...otherRules
}

// now I have a fairly strong typed object RuleMap
RuleMap.isDate('foo', { format: "DD/MM/YYYY" }); // no problem
RuleMap.required('foo', { format: "DD/MM/YYYY" }); // throws error

但是当我尝试这样做时

const myRules: Rule = {
  isNumber: true
}

(Object.keys(myRules) as (keyof Rule)[]).forEach((ruleName) => {
  // Expect this to be working
  RuleMap[ruleName](someValue, myRules[ruleName]);
  // This throws error
  // 'true | { format: string }' is not assignable to type '(true & { format: string })'
});

例如: 指定isDate 时,RuleMap 强制我输入准确的isDate 函数选项。

当循环 Rule 时,我希望打字稿知道我正在将确切的 ruleName 的 ruleOption 传递给函数。

有可能吗?如果是这样,我应该怎么做?

【问题讨论】:

  • 这里的问题是打字稿正在工作,它会抛出错误编译,因为你不能在不检查类型的情况下在同一个循环中为不同的规则应用相同的参数。
  • 如何让 typescript 知道我通过了正确的规则?事实上我正在通过正确的规则
  • 您正在寻找io-ts

标签: typescript loops types typescript-typings


【解决方案1】:

问题恰恰在于声明myRulesRule,这意味着TS在编译时并不知道这个对象中的字段是什么,它把它当作Rule,所以字段可以或不在。

您的循环是键入的,因为它会遍历来自Rule 而不是来自typeof myRules 的所有键,实际上它们的一部分不存在,但类型系统没有看到。在值级别很明显,您通过myRules,如果您这样做,字段就在那里,但对于类型级别myRules 只是Rule,并且具有可以未定义值的键。这与以下简化示例中发生的情况完全相同:

const ruleName: "required" | "isNumber" | "isDate" = 'isDate';
const value = myRules[ruleName]; // value can be undefined!

以上正是forEach内部发生的情况,键被键入为Rule的键,值可以是undefined与调用函数的要求相冲突,要求表示为-options: Required&lt;Rule&gt;[key],所以它不允许undefined。这里出现了一个错误。

好的,那我们能做些什么呢?简单的解决方案是使用const 直接使用myRules 类型:

const myRules = {
  isNumber: true
} as const; // pay attention here

(Object.keys(myRules) as (keyof typeof myRules)[]).forEach((ruleName) => {
  const value = 1; // example
  RuleMap[ruleName](value, myRules[ruleName]);
});

工作没有错误,因为ruleName 总是在类型中,所以myRules[ruleName] 永远不会未定义。 const 允许类型系统使用与运行时对象完全相同的形状。


我假设您在更真实的环境中需要它。在这种情况下,我们需要具有泛型,这将允许使用精确缩小的Rule 接口类型。考虑下面的例子:

type OnlyKeysInRule<T extends Rule> = {
  [K in keyof Rule]: T[K]
} & {
  [K in Exclude<keyof T, keyof Rule>]: never
}
function doStaff<R extends Rule>(rules: OnlyKeysInRule<R>) {
  (Object.keys(rules) as (keyof OnlyKeysInRule<R> & keyof typeof RuleMap)[]).forEach((ruleName) => {
    const value = 1; // example
    RuleMap[ruleName](value,  rules[ruleName]);
  });
}
doStaff(myRules) // is ok
doStaff({isNumber: true, isHelicopter: false}) // error as expected - `isHelicopter` is not define in RuleMap

解释:

  • OnlyKeysInRule 是独家地图,上面写着 - 你可以传递只有 Rule 中的字段的对象。为什么它很重要 - 因为R extends Rule 意味着类型 R 可以有附加字段,这意味着 RuleMap[ruleName] 不是安全操作,因为 ruleName 可以是 RuleMap 中不存在的附加字段,这种类型可以防止这种情况。李>
  • as (keyof OnlyKeysInRule&lt;R&gt; &amp; keyof typeof RuleMap) 我们说我们只通过 RuleMapR 中的键进行循环。这里 TS 无法推断出 OnlyKeysInRule 已经足以防止这种情况发生,这就是为什么需要使用 union。

【讨论】:

  • 谢谢!我确实需要该示例用于真实环境!
猜你喜欢
  • 1970-01-01
  • 2018-09-15
  • 1970-01-01
  • 1970-01-01
  • 2019-01-16
  • 2022-06-13
  • 2020-10-11
相关资源
最近更新 更多