【问题标题】:Multiple indexer in Indexable types in TypeScriptTypeScript 中可索引类型中的多个索引器
【发布时间】:2021-04-05 14:11:40
【问题描述】:

我在 TypeScript 中阅读 Indexable Types。我正在经历这个例子。

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
  [x: number]: Animal;
// Numeric index type 'Animal' is not assignable to string index type 'Dog'.
  [x: string]: Dog;
}

我无法理解为什么上面的代码会产生问题。我在网上浏览了很多文章,但无济于事。谁能用至少一个例子用简单的话解释我上面的代码会如何产生问题,而这样做不会。

interface Okay {
      [x: string]: Dog;
      [x: number]: Animal;
}

如果这里也能与 C++、Java(超类、子类)等语言建立关联,将不胜感激。

【问题讨论】:

    标签: javascript typescript types interface


    【解决方案1】:

    TL;DR:

    • JS/TS 中的对象实际上没有number 类型的键;它们实际上是一种特殊类型的 string 键。
    • TS 中的索引签名不能与其他索引签名或属性冲突。
    • 由于“number”键实际上是一种特殊的string 键,因此任何number 索引签名属性都必须可分配给string 索引签名属性(如果存在)。

    一个可能令人困惑的问题是 JavaScript 没有真正的数字键。 JavaScript 中的键要么是symbols 要么是strings...即使对于通常被认为具有数字索引的数组也是如此。

    当您使用除symbol 以外的任何类型的键对对象进行索引时,JS 会将其强制转换为string(如果它还不是一个)。因此,虽然您可以想到在索引 0 处有一个元素的单元素数组,但在技术上说它在索引 "0" 处有一个元素更正确:

    const arr: [string] = [""];
    const arrKeys: string[] = Object.keys(arr);
    console.log(arrKeys) // ["0"]
    console.log(arrKeys.includes(0 as any)) // false
    console.log(arrKeys.includes("0")) // true
    arr[0] = "foo";
    arr["0"] = "bar";
    console.log(arr[0]); // "bar";
    

    TypeScript 允许您指定键类型 number 的索引签名来表示类似数组的元素访问,但它不会改变数字键被强制转换为字符串的事实。如果number 索引签名使用NumericString 之类的类型会更准确。但是在 TypeScript 中没有公开这样的类型。 (顺便说一句:好吧,TypeScript 4.1 的 template literal types 实际上为您提供了一种将这种类型表示为的方法

    type NumericString = `${number}`;
    

    但你不能真正将它用作索引键类型而不是索引签名,所以这一点有点没有实际意义。)所以虽然一般number 不是string 的子类型,但对于 keys,你可以把number看作string的一种类型。


    下一个可能的混淆点是 TypeScript 不会将索引签名视为“例外”。如果您添加索引签名,则它不得与任何其他属性冲突。 (有关详细信息,请参阅 this q/a。)例如,以下类型的 baz 成员存在问题:

    interface A {
      foo: string; // okay
      bar: number; // okay
      baz: boolean; // <-- error! boolean not assignable to string | number
      [k: string]: string | number;
    }
    

    索引签名[k: string]: string | number 的意思是“如果您使用string 类型的任意键A 读取属性,您将获得string | number 类型的值。” foo 属性是兼容的,因为键 "foo"string,而值类型 string 可分配给 string | numberbar 属性是兼容的,因为键 "bar"string,而值类型 number 可分配给 string | number。但是baz 是错误的。键"baz"是字符串,但是违反了索引签名; boolean 不能分配给 string | number

    你不能使用像上面这样的索引签名来表示“好吧,键 "baz" 处的属性是 boolean 但每个 other string-keyed 属性都有一个 @ 类型的值987654373@。如果能有这样的说法就好了(请参阅microsoft/TypeScript#17687 以获取此请求),但索引签名不能那样工作。

    认为键"baz"string 的特例会有所帮助,因此它的属性类型可以是string | number 的特例,而boolean 不起作用。


    所以,让我们把它们放在一起:

    interface NotOkay {
      [x: number]: Animal; // error!  Animal not assignable to Dog
      [x: string]: Dog;
    }
    

    假设我有一个NotOkay 类型的值,我用它的一个键对其进行索引:

    const notOkay: NotOkay = {
      str: dog,
      123: animal
    }
    const randomKey = Object.keys(notOkay)[Math.random() < 0.5 ? 0 : 1]; // string
    const randomProp = notOkay[randomKey] // Dog?
    console.log(randomProp.breed.toUpperCase()); // either LAB or runtime error?
    

    这可能会导致运行时错误,因为键 123 实际上是 "123",即 stringNotOkaystring 索引签名表示 string 键处的每个属性都将是 Dog 类型。但是等等,number 索引签名与它不兼容。如果我继续将每个string-indexed 属性都视为Dog 类型,我会遇到一些假设Dogs 没有breed 的问题。

    因此,number 索引签名是一个问题。考虑到键类型numberstring 的特例,因此它的属性类型可以是Dog 的特例,而Animal 不起作用。


    如果您切换DogAnimal,问题就会消失:

    interface Okay {
          [x: string]: Animal;
          [x: number]: Dog;
    }
    const okay: Okay = {
      str: animal,
      123: dog
    }
    const randomKey2 = Object.keys(okay)[Math.random() < 0.5 ? 0 : 1]; // string
    const randomProp2 = okay[randomKey2] // Animal
    console.log(randomProp2.name.toUpperCase()); // no error here, FIDO or FLUFFY
    

    由于number 键是string 键的特例,而DogAnimal 的特例,所以一切正常。您的未知string 密钥的属性已知为Animal。如果你把Dog 当作Animal 对待,没关系,因为它是一个。


    Playground link to code

    【讨论】:

    • 很好的解释! TS采用这种行为的原因是什么,因为数组使用数字和字符串作为索引,并且它们应该具有相同的类型签名,那么TS是否会检查不同的子类型的数字索引?
    • 我不确定我是否理解这个问题。 TS 中的数组没有string 索引签名。它们具有元素的number 索引签名,以及一堆单独的字符串索引,例如lengthpush,它们都不是类似数字的字符串。如果数组有一个string 索引签名,那么它必须至少与所有其他属性类型的并集一样宽……但它没有,所以没有这样的问题。
    • 你的评论和这个答案很有意义。
    • @KenpachiZaraki 在Okay 中,如果你知道你有一个number 密钥,你可以访问breed,比如this。但至于用例,我不确定为什么有人实际上想要numberstring 索引......也许是像this 这样的expando 数组?我认为数字和字符串索引签名的行为与其说是一种设计,不如说是索引签名规则的逻辑结果。
    • 这是索引签名的另一个问题(并且可能超出了这个问题的范围):undefined 默认情况下不包含在索引签名属性的域中,即使不是所有键保证在场。请参阅 microsoft/TypeScript#13778 进行讨论,并参阅 TS4.1 中添加的 --noUncheckedIndexedAccess 编译器标志 doc link 作为可能的处理方式。
    猜你喜欢
    • 1970-01-01
    • 2020-08-01
    • 2021-12-30
    • 1970-01-01
    • 2021-11-20
    • 2017-06-11
    • 2019-06-19
    • 2013-01-28
    • 2023-03-13
    相关资源
    最近更新 更多