我可以在 TypeScript 中创建一个对每个键值对都是类型安全的键值字典吗?

Posted

技术标签:

【中文标题】我可以在 TypeScript 中创建一个对每个键值对都是类型安全的键值字典吗?【英文标题】:Can I make a key-value dictionary in TypeScript that's typesafe for every key-value pair? 【发布时间】:2021-05-10 02:18:30 【问题描述】:

这个问题有点牵强,最好通过探索一个基本的状态系统来解决,所以和我一起在这里走一分钟。假设我有这个状态类:

class AccountState 
  public id: string;
  public displayName: string;
  public score: number;

从jcalz's work here,我知道我可以构建一个以类型安全的方式引用任何 AccountState 属性的函数——我可以获取属性名称和值,并使用泛型对该值施加属性自己的类型限制,这非常棒令人印象深刻:

class Store 
  state = new AccountState();

  mutate<K extends keyof AccountState>(property: K, value: AccountState[K]): void 
    this.state[property] = value;
  


const store = new Store();
store.mutate('displayName', 'Joseph Joestar'); // ok
store.mutate('displayName', 5); // not ok: surfaces the below typescript error
// ts(2345) Argument of type 'number' is not assignable to parameter of type 'string'.

使用 jcalz 的回答中的ValueOf&lt;T&gt;,我还可以建模一个类型安全的键值字典。对我来说,向您展示它的工作原理以及它的缺点可能是最简单的:

type ValueOf<T> = T[keyof T];

class Store 
  state = new AccountState();

  mutateMany(updates:  [key in keyof AccountState]?: ValueOf<AccountState> ): void 
    Object.keys(updates).forEach(property => 
      const value = updates[property];
      (this.state[property] as any) = value;
    );
  


const store = new Store();
store.mutateMany( displayName: 'Joseph Joestar', score: 5 ); // ok
store.mutateMany( displayName: 1000, score: 'oh no' ); // unfortunately, also ok
store.mutateMany( score: true ); // not ok, surfaces the below error
// ts(2322) Type 'boolean' is not assignable to type 'ValueOf<AccountState>'.
// (if AccountState had a boolean property, this would be allowed)

第二个mutateMany() 是个问题。如您所见,我可以要求密钥是 AccountState 的某些属性。我还可以要求该值对应于 AccountState 上的某些属性,因此它必须是 string | number。但是,并不要求该值与属性的实际类型相对应。

如何使字典完全类型安全,例如允许 displayName: 'a', score: 1 ,但不允许 displayName: 2, score: 'b'

我考虑过声明一个 AccountStateProperties 接口,它简单地重复所有这些属性及其值,然后定义 mutateMany(updates: AccountStateProperties),但这会为更多涉及的状态对象添加大量代码重复。直到今天我才知道我可以做一些这些事情,我想知道打字系统是否有一些东西我可以在这里利用来使这本字典在没有这种方法的情况下完全类型安全。

【问题讨论】:

【参考方案1】:

mutateMany 方法[key in keyof AccountState]?: ValueOf&lt;AccountState&gt; 中,您是说对于任何key,值的类型可以是AccountState 具有的任何类型。如果您尝试使用不在 AccountState 中的内容(如 true)进行更新,您会看到这一点。

相反,我相信你想要:

mutateMany(updates:  [key in keyof AccountState]?: AccountState[key] )

这表示key 的值还应与keyAccountState 的类型相匹配,而不仅仅是AccountState 的任何值类型。

[编辑:如果您查看链接的答案,以“为了确保键/值对在函数中正确“匹配”的部分,您应该使用泛型以及查找类型。 ..”描述了这一点]

【讨论】:

谢谢!这完美地工作。 :) 我使用了您在 mutate() 案例中引用的部分,但我不知道如何让它在 mutateMany() 案例中为我工作。【参考方案2】:

对于mutateMany(),我认为有效且不需要太多不安全性的版本是让updates 的类型为Pick&lt;AccountState, K&gt;,其中K 是constrained成为来自AccountState 的键,以及我们使用Pick&lt;T, K&gt; utility type 的位置。 Pick&lt;AccountState, K&gt; 类型的值将在 K 的键处具有来自 AccountState 的所有属性。

以下是写法:

class Store 
    state = new AccountState();
    mutateMany<K extends keyof AccountState>(updates: Pick<AccountState, K>): void 
        (Object.keys(updates) as Array<K>).forEach(<P extends K>(property: P) => 
            this.state[property] = updates[property];
        );
    

这里发生的唯一不安全的事情是我们assert Object.keys(updates) 将只返回K 中的密钥。编译器实际上并不知道这一点,因为 TypeScript 中的对象类型没有关闭; updates 的属性可能比编译器知道的要多。请参阅this question and answer 了解更多信息。

让我们看看它是如何工作的:

const store = new Store();
store.mutateMany( displayName: 'Joseph Joestar', score: 5 ); // ok
store.mutateMany( displayName: 1000, score: 'oh no' ); // error!
// --------------> ~~~~~~~~~~~  ----> ~~~~~
// number isn't string,         string isn't number

store.mutateMany( displayName: "okay", randomProp: 123 );  // error!
// -----------------------------------> ~~~~~~~~~~~~~~~
// Object literal may only specify known properties

看起来不错。错误出现在我们预期的地方。


请注意,由于excess property checks,如果您传入对象字面量,上面会防止updates 中的额外键问题。但是如果你不使用新的对象字面量,就有可能做一些奇怪的事情:

const badMutation = 
    displayName: "okay",
    randomProp: 123

const aliasedMutation:  displayName: string  = badMutation;
store.mutateMany(aliasedMutation); // no error here

这不太可能,所以你可能不在乎。如果您确实关心,TypeScript 中的类型系统无法真正阻止它,因此您需要在运行时通过一些额外的检查来保护自己,例如

        if (["id", "displayName", "score"].includes(property))
            this.state[property] = updates[property];

Playground link to code

【讨论】:

这太棒了。今天我了解了 Pick!

以上是关于我可以在 TypeScript 中创建一个对每个键值对都是类型安全的键值字典吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何从键值对列表中创建 Spark Row

如何在 TypeScript 中创建本地模块

在 TypeScript 中创建电子菜单?

是否可以在 TypeScript 中创建严格的“A 或 B”类型? [复制]

如何像在 babelrc 中创建依赖别名一样在 typescript 中创建类型别名?

在mysql中创建两个用外键链接的表,插入、删除和更新