【问题标题】:Can I make a key-value dictionary in TypeScript that's typesafe for every key-value pair?我可以在 TypeScript 中创建一个对每个键值对都是类型安全的键值字典吗?
【发布时间】: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),但这会为更多涉及的状态对象添加大量代码重复。直到今天我才知道我可以做一些这些事情,我想知道打字系统是否有一些东西我可以在这里利用来使这本字典在没有这种方法的情况下完全类型安全。

【问题讨论】:

    标签: typescript dictionary types


    【解决方案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;,其中Kconstrained成为来自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!
    猜你喜欢
    • 2014-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多