将泛型与索引类型相结合

Posted

技术标签:

【中文标题】将泛型与索引类型相结合【英文标题】:Combining generics with index type 【发布时间】:2020-04-07 07:08:12 【问题描述】:

既然这样有效:

const f = <T extends string>(x: T) => x;
f("");

interface Dictionary<T>  [key: string]: T; 
const dict: Dictionary<number> =  a: 1 ;

我希望以下代码也能正常工作:

interface MyRecord<Key extends string, Value>  [_: Key]: Value ;

但编译器报告_:

An index signature parameter type must be 'string' or 'number'.

Key extends string 更改为Key extends string | number 没有任何作用(同样的错误)。

它失败的原因是什么?看起来如何是正确的解决方案?(最好不要使用Any 和类似的。)

编辑1:

type XY = 'x' | 'y';
const myXY: XY = 'x';
const myString: string = myXY;

由于这可行,我假设索引类型具有相同的保留(string 的子集可以构成索引类型所需的string 角色)。

【问题讨论】:

What is the Record type in typescript? 的可能重复(好吧,无论如何都是答案) TypeScript - can a generic constraint provide “allowed” types?的可能重复 两者似乎都不是重复的。 ***.com/questions/51936369/… 正在询问 Record 是什么,并且使用非常相似(如果不相同)的代码,这些代码似乎对他们有用,而对我却不适用。 ***.com/questions/46885489/… 的答案实际上说要使用映射类型,这似乎与我的(不工作的)代码所做的完全一样。 您没有使用映射类型[P in K]: V,您正在尝试使用索引签名[p: K]: V。它们看起来很相似,但却是不同的东西(后者是一个错误...K 只能是索引签名中的stringnumber 哦,你是对的。仍然不明白为什么它不适用于索引类型。为什么它说Key extends string 不是string 的子集? 'a'|'b'(或任何字符串联合)不是string 的子集吗? 【参考方案1】:

我们来谈谈index signature types 和mapped types。它们具有相似的语法并做相似的事情,但它们并不相同。以下是相似之处:

它们都是代表一系列属性的对象类型

语法:索引签名和映射类型都在对象类型中使用带括号的 keylike 表示法,如[<b><i>Some Key-like Expression</i></b>]: T

现在来看看区别:

索引签名

索引签名描述对象类型或接口的一部分,表示任意数量的属性相同类型,带有来自某个特定的键键类型。目前,此密钥类型只有两种选择:stringnumber

语法:索引签名的语法如下所示:

type StringIndex<T> = [dummyKeyName: string]: T
type NumberIndex<T> = [dummyKeyName: number]: T 

有一个虚拟键名(上面的dummyKeyName)可以是任何你想要的并且在括号之外没有任何意义,后面是stringnumber的类型注释(:) .

对象类型的一部分:索引签名可以与对象类型或接口中的其他属性一起出现:

interface Foo 
  a: "a",
  [k: string]: string

任意数量的属性:可索引类型的对象不需要对每个可能的键都有一个属性(对于stringnumber,除了Proxy 对象之外,这甚至是不可能做到的)。相反,您可以将包含任意数量的此类属性的对象分配给可索引类型。请注意,当您从可索引类型读取属性时,编译器将假定该属性存在(而不是 undefined),即使启用了 --strictNullChecks,even though this is not strictly type safe。示例:

type StringDict =  [k: string]: string ;
const a: StringDict = ; // no properties, okay
const b: StringDict =  foo: "x", bar: "y", baz: "z" ; // three properties, okay
const c: StringDict =  bad: 1, okay: "1" ; // error, number not assignable to boolean

const val = a.randomPropName; // string
console.log(val.toUpperCase()); // no compiler warning, yet
// "TypeError: val is undefined" at runtime

同一类型的属性:索引签名中的所有属性必须是同一类型;类型不能是特定键的函数。因此,“属性值与其键相同的对象”不能用索引签名表示为比[k: string]: string 更具体的任何东西。如果你想要一个接受a: "a" 但拒绝b: "c" 的类型,你不能用索引签名来做到这一点。

只允许使用stringnumber 作为键类型:目前可以使用string 索引签名来表示类字典类型,或者使用number 索引签名来表示数组样的类型。而已。您不能将类型扩大到 string | number 或将其缩小到特定的一组 stringnumber 文字,如 "a"|"b"1|2。 (您关于为什么它应该接受更窄集合的推理是合理的,但这不是它的工作原理。规则是“索引签名参数类型必须be stringnumber”。)有正在对relax this restriction and allow arbitrary key types in an index signature, see microsoft/TypeScript#26797 进行一些工作,但至少目前看来是stalled。

映射类型

另一方面,映射类型描述整个对象类型,而不是接口,表示特定的一组属性可能变化的类型,带有特定键类型的键。 您可以为此使用任何键类型,尽管文字联合是最常见的(如果您使用 stringnumber,那么映射类型的那部分变成......猜猜看? 一个索引签名!)接下来我将只使用文字的并集作为键集。

语法:映射类型的语法如下所示:

type Mapped<K extends keyof any> = [P in K]: SomeTypeFunction<P>;
type SomeTypeFunction<P extends keyof any> = [P]; // whatever

引入了一个新的类型变量P,它遍历键组合in和键集K的每个成员。新类型变量仍在属性值SomeTypeFunction&lt;P&gt; 的范围内,即使它位于方括号之外。

整个对象类型:映射类型是整个对象类型。它不能与其他属性一起出现,也不能出现在界面中。这就像一个联合或交集类型:

interface Nope 
    [K in "x"]: K;  // errors, can't appear in interface

type AlsoNope = 
    a: string,
    [K in "x"]: K; // errors, can't appear alongside other properties

一组特定的属性:与索引签名不同,映射类型必须在键集中的每个键都具有一个属性。 (如果该属性碰巧是可选的,或者因为它是从具有可选属性的类型映射的,或者因为您使用 ? 修饰符将属性修改为可选,则例外情况):

type StringMap =  [K in "foo" | "bar" | "baz"]: string ;
const d: StringMap =  foo: "x", bar: "y", baz: "z" ; // okay
const e: StringMap =  foo: "x" ; // error, missing props
const f: StringMap =  foo: "x", bar: "y", baz: "z", qux: "w" ; // error, excess props

属性类型可能会有所不同:因为迭代键类型参数在属性类型的范围内,您可以根据键来改变属性类型,如下所示:

type SameName =  [K in "foo" | "bar" | "baz"]: K ;
/* type SameName = 
    foo: "foo";
    bar: "bar";
    baz: "baz";
 */

可以使用任何密钥集:您不限于stringnumber。您可以使用任何一组string 文字或number 文字,甚至symbol 值(但使用起来更加复杂,所以我忽略了这一点)。您也可以在其中使用stringnumber,但当发生这种情况时,您会立即获得索引签名:

type AlsoSameName =  [K in "a" | 1]: K ;    
/* type AlsoSameName = 
    a: "a";
    1: 1;   
 */
const x: AlsoSameName =  "1": 1, a: "a" 

type BackToIndex =  [K in string]: K 
/* type BackToIndex = 
    [x: string]: string;
*/
const y: BackToIndex =  a: "b" ; // see, widened to string -> string

而且由于可以使用任何键集,它可以是通用的:

type MyRecord<Key extends string, Value> =  [P in Key]: Value ;

这就是你将如何制作MyRecord。它不能是可索引的类型;只有一个映射类型。请注意the built-in Record&lt;K, T&gt; utility type 本质上是相同的(它允许K extends string | number | symbol),因此您可能希望使用它而不是您自己的。

好的,希望对您有所帮助;祝你好运!

Link to code

【讨论】:

【参考方案2】:

您可以使用 typescript 的 Record&lt;Key, Value&gt; 实用程序代替索引签名。

interface MyObject<K extends string = string> 
  someProperty: Record<K, any>;

【讨论】:

以上是关于将泛型与索引类型相结合的主要内容,如果未能解决你的问题,请参考以下文章

将泛型与 GWT-RPC 一起使用未按预期工作

是否可以将泛型重载限制为属性类型?

Java 将泛型类型与 Void 进行比较

如何将泛型类型参数限制为 System.Enum [重复]

将泛型类型转换为具体类型时出错

Typescript 从具有泛型类型的对象索引调用函数