5

我想根据以下条件为对象的字段分配一个数字:

function maybeANumber(): number | undefined {
  const n = Math.random();
  return n > 0.5 ? n : undefined;
}

function maybeSetNumber(target: any, field: any) {
  const num = maybeANumber();
  if (num !== undefined) {
    target[field] = num;
  }
}

这很有效,因为我使用any得很自由,但是我怎样才能正确输入它,以便它检测到类型错误:


interface Foo {
  a: string,
  b: number,
}

const foo: Foo = { a: "", b: 0 };

maybeSetNumber(foo, "a"); // Should be a compile-time error.
maybeSetNumber(foo, "b"); // Should be a ok.

有没有办法做到这一点?

编辑:重要说明:字段名称是静态的。我不需要它来处理任意字符串。我已经尝试了很多东西,keyof但无法弄清楚。

4

4 回答 4

5

您可以在签名中使用泛型maybeSetNumber()来表示它field属于通用属性键类型 ( K extends PropertyKey),并且属于在该键处target具有值的类型(使用实用程序类型):numberRecord<K, number>Record

function maybeSetNumber<K extends PropertyKey>(target: Record<K, number>, field: K) {
  const num = maybeANumber();
  if (num !== undefined) {
    target[field] = num;
  }
}

这将给出你想要的行为:

maybeSetNumber(foo, "a"); // error!
// ----------> ~~~
// Types of property 'a' are incompatible.
maybeSetNumber(foo, "b"); // okay

警告:TypeScript 并不完美所以如果你开始使用比以下更窄的类型,这仍然会让你做一些不安全的事情number

interface Oops { x: 2 | 3 }
const o: Oops = { x: 2 };
maybeSetNumber(o, "x"); // no error, but could be bad if we set o.x to some number < 1

也可以使签名使得上面的错误是 on"a"而不是 on foo。这种方式更复杂,并且至少需要一个类型断言,因为编译器不理解其中的含义:

type KeysMatching<T, V> = { [K in keyof T]: V extends T[K] ? K : never }[keyof T]
function maybeSetNumber2<T>(target: T, field: KeysMatching<T, number>) {
  const num = maybeANumber();
  if (num !== undefined) {
    target[field] = num as any; // need a type assertion here
  }
}

maybeSetNumber2(foo, "a"); // error!
// ----------------> ~~~
// Argument of type '"a"' is not assignable to parameter of type '"b"'.
maybeSetNumber2(foo, "b"); // okay

这不会遇到与 , 相同的Oops问题

maybeSetNumber2(o, "x"); // error!

但仍然可能存在围绕健全性的边缘情况。TypeScript 通常假设如果您可以X从属性中读取类型的值,那么您可以将类型的值写入X该属性。这很好,直到它不是。在任何情况下,其中任何一个都会比any.

Playground 代码链接

于 2020-09-23T20:28:46.980 回答
2

我会这样输入maybeSetNumber

function maybeSetNumber<F extends string>(target: { [key in F]: number }, field: F) {
  const num = maybeANumber();
  if (num !== undefined) {
    target[field] = num;
  }
}

游乐场链接

于 2020-09-23T20:23:34.937 回答
0

这里的其他答案实际上很有用——我只想提供一个根本原因,说明为什么没有类型断言就不可能做到这一点。为了执行 set target[field]: O[K] = num,我们当然需要断言O[K]: number。然而:

  • 泛型的唯一静态约束必须全部包含在函数头的尖括号中;
  • 这些只能采用单个泛型类型变量和替代关系的形式;和
  • 该语言的设计将约束从尖括号应用到主体的其余部分,但不是相反:即不可能在主体或参数中创建约束,这些约束将暗示关于这些尖括号中的泛型的任何向后。

不幸的是,我们想要断言 about O[K],它不是一个单一的类型变量,而 arguments/body 也帮不上忙。

从上下文到主体的约束向前流动在其他具有类型上下文的遥远语言中很常见——例如 Haskell。我对类型检查器的实现不够熟悉,不知道这是否是关于可判定性的理论问题,但无论如何,底线是 Typescript 的类型上下文目前还不够强大,无法让我们做出这个断言。

于 2020-09-23T21:29:33.100 回答
-1

在没有联合类型的语言中,处理这个问题的一种方法是多个返回值,在支持它的语言中(因为你真的试图返回两个值,一个数字和一个是否使用数字的指示符,你正在尝试将返回值的类型重新用作附加到返回数字的属性标志)。在这种情况下,如果“使用”标志为假,则返回一个虚拟整数。

我在这里找到了如何在 Javascript 中执行此操作的示例:Return multiple values in JavaScript?

于 2020-09-23T20:54:36.560 回答