【问题标题】:Narrowing a string to a key of an object将字符串缩小为对象的键
【发布时间】:2021-05-15 23:19:54
【问题描述】:

注意:TypeScript 使用tsc --strict 调用下面显示的所有代码。

给定一个单例对象o

const o = {
  foo: 1,
  bar: 2,
  baz: 3,
};

如果我有一个在编译时无法知道的字符串值(比如来自用户输入),我想安全地使用该字符串来索引o。我不想为o 添加索引签名,因为它不是动态的或可扩展的——它总是有这三个键。

如果我尝试简单地使用字符串来索引o

const input = prompt("Enter the key you'd like to access in `o`");

if (input) {
  console.log(o[input]);
}

TypeScript 按预期报告此错误:

error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: number; bar: number; baz: number; }'.
  No index signature with a parameter of type 'string' was found on type '{ foo: number; bar: number; baz: number; }'.

     console.log(o[input]);
                 ~~~~~~~~

如果我尝试更改条件以在运行时验证 input 的值是 o 的键:

if (input && input in o) {
  console.log(o[input]);
}

这不足以让 TypeScript 相信操作是安全的,编译器也会报同样的错误。

但是,如果我在custom type predicate 中封装相同的逻辑来检查input 是否是o 的键,那么程序将按预期编译和工作:

function isKeyOfO(s: string): s is keyof typeof o {
  return s in o;
}

if (input && isKeyOfO(input)) {
  console.log(o[input]);
}

我的问题是:还有其他方法可以将string 类型的值缩小为keyof typeof o 类型的值吗?我希望有另一种更简洁的方法。我也对使用动态字符串索引对象的用例的通用解决方案感兴趣,这样我就不需要特定于o 的类型谓词。

【问题讨论】:

    标签: typescript type-narrowing


    【解决方案1】:

    我不认为编译器可以验证为类型安全的任何更简洁的方法。您可以像这样手动枚举可能性:

    if (input === "foo" || input === "bar" || input === "baz") {
        console.log(o[input]); // okay
    }
    

    但你会发现试图减少冗余只会导致更多错误:

    if ((["foo", "bar", "baz"] as const).includes(input)) { } // error!
    // -----------------------------------------> ~~~~~
    // Argument of type 'string | null' is not assignable to parameter
    // of type '"foo" | "bar" | "baz"'.
    

    (请参阅this question 了解更多信息,以及microsoft/TypeScript#36275 了解为什么它甚至不能充当类型保护)。

    所以我一般不建议这样做(但见下文)。


    microsoft/TypeScript#43284 有一个公开的建议,允许 k in o 充当 k 上的类型保护,除了当前支持它充当 o 上的类型保护(请参阅@987654324 @)。如果实现了,您的原始检查 (input && input in o) 将正常运行而不会出现错误。

    GitHub 中的 issue 目前已打开并标记为“Awaiting More Feedback”;因此,如果您想在某个时候看到这种情况发生,您可能想去那里,给它一个 ?,如果您认为它特别引人注目,请描述您的用例。


    我个人认为这里最好的解决方案可能是您使用s is keyof typeof o 类型谓词的用户定义类型保护函数,因为它明确告诉编译器和任何其他开发人员您打算s in o 在此范围内缩小s方式。


    请注意,由于对象类型是可扩展,因此您的自定义类型保护和 microsoft/TypeScript#43284 建议的自动类型保护在技术上都是不合理的; typeof o 类型的值很可能具有未知的属性,因为 TypeScript 中的对象类型是 extendibleopen

    const p = {
        foo: 1,
        bar: 2,
        baz: 3,
        qux: "howdy"
    };
    const q: typeof o = p; // no error, object types are extendible
    
    function isKeyOfQ(s: string): s is keyof typeof q {
        return s in q;
    }
    
    if (input && isKeyOfQ(input)) {
        input // "foo" | "bar" | "baz"
        console.log(q[input].toFixed(2)); // no compiler error
        // but what if input === "qux"?
    }
    

    在这里,编译器将q 视为与o 具有相同类型。这是真的,尽管有一个名为qux 的额外属性。 isKeyOfQ(input) 会错误地将 input 缩小到 "foo" | "bar" | "baz"...,因此编译器认为 q[input].toFixed(2) 是安全的。但是由于q.qux该属性值是string类型,而其他属性值是number类型,因此潜伏着危险。

    在实践中,这种不健全并不是什么大问题;有一些intentionally unsound behaviors in TypeScript 认为便利性和开发人员的生产力更为重要。

    但是你应该知道你在做什么,所以你只在你的对象的出处已知的情况下使用这种缩小;如果你从一些不受信任的来源获得q,你可能想要一些更可靠的东西......例如input === "foo" || input === "bar" || input === "baz"或通过["foo", "bar", "baz"].includes(input)实现的其他一些用户定义的类型保护处理:

    function isSafeKeyOfQ(s: string): s is keyof typeof q {
        return ["foo", "bar", "baz"].includes(s);
    }
    
    if (input && isSafeKeyOfQ(input)) {
        console.log(q[input].toFixed(2)); // safe
    }
    

    Playground link to code

    【讨论】:

    • 感谢您提供丰富的上下文!不健全的问题非常有趣。我在静态类型方面的经验从未包括像 JavaScript 对象那样动态的构造,所以我不会想到它。
    【解决方案2】:

    我相信这就是你所追求的那种方法......

    const o = {
      foo: 1,
      bar: 2,
      baz: 3,
    } as const;
    
    function printRequestedKey<Lookup extends Readonly<object>>(lookup: Lookup){
      const input = prompt("Enter the key you'd like to access");
      if (input !== null && input in lookup) {
        console.log(lookup[input as keyof typeof lookup]);
      }
    }
    
    printRequestedKey(o);
    
    

    根据 Aadmaa 的回答,它会将 const 添加到您对 o 的定义中。它还引入了 Generic 绑定,因此如果您正在执行控制台日志记录以外的任何操作,则返回的值可以是源对象的一些投影。 javascript 保护 input in lookup 以及 Readonly 对象的使用让我确信显式转换 input as keyof typeof lookup 不会引入运行时错误。我添加了一个空检查,因为提示可以返回空(可能通过键转义?)。

    【讨论】:

      【解决方案3】:

      你的方法很好。如果你愿意,你可以做类似的事情

      const o = {
          foo: 1,
          bar: 2,
          baz: 3,
        } as const;
      
        type KeyO = keyof typeof o;
      
        const input = prompt("Enter the key you'd like to access in `o`");
      
        if (input in Object.keys(o)) {
            const typedInput = input as KeyO;
        }
      

      【讨论】:

      • 这似乎不安全,因为即使您删除了 input in Object.keys(o) 条件,它仍然会编译。 (它也没有检查inputnull 的情况,但我认为这只是示例代码中的一个疏忽。)类型断言(input as Key0)似乎通过强制编译器丢弃类型安全性将值视为可能不是的类型。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-09-27
      • 2014-11-28
      • 1970-01-01
      • 2022-12-09
      • 1970-01-01
      • 1970-01-01
      • 2022-11-10
      相关资源
      最近更新 更多