将泛型与索引类型相结合
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
只能是索引签名中的string
或number
)
哦,你是对的。仍然不明白为什么它不适用于索引类型。为什么它说Key extends string
不是string
的子集? 'a'|'b'
(或任何字符串联合)不是string
的子集吗?
【参考方案1】:
我们来谈谈index signature types 和mapped types。它们具有相似的语法并做相似的事情,但它们并不相同。以下是相似之处:
它们都是代表一系列属性的对象类型
语法:索引签名和映射类型都在对象类型中使用带括号的 keylike 表示法,如[<b><i>Some Key-like Expression</i></b>]: T
现在来看看区别:
索引签名
索引签名描述对象类型或接口的一部分,表示任意数量的属性相同类型,带有来自某个特定的键键类型。目前,此密钥类型只有两种选择:string
或 number
。
语法:索引签名的语法如下所示:
type StringIndex<T> = [dummyKeyName: string]: T
type NumberIndex<T> = [dummyKeyName: number]: T
有一个虚拟键名(上面的dummyKeyName
)可以是任何你想要的并且在括号之外没有任何意义,后面是string
或number
的类型注释(:
) .
对象类型的一部分:索引签名可以与对象类型或接口中的其他属性一起出现:
interface Foo
a: "a",
[k: string]: string
任意数量的属性:可索引类型的对象不需要对每个可能的键都有一个属性(对于string
或number
,除了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"
的类型,你不能用索引签名来做到这一点。
只允许使用string
或number
作为键类型:目前可以使用string
索引签名来表示类字典类型,或者使用number
索引签名来表示数组样的类型。而已。您不能将类型扩大到 string | number
或将其缩小到特定的一组 string
或 number
文字,如 "a"|"b"
或 1|2
。 (您关于为什么它应该接受更窄集合的推理是合理的,但这不是它的工作原理。规则是“索引签名参数类型必须be string
或number
”。)有正在对relax this restriction and allow arbitrary key types in an index signature, see microsoft/TypeScript#26797 进行一些工作,但至少目前看来是stalled。
映射类型
另一方面,映射类型描述整个对象类型,而不是接口,表示特定的一组属性可能变化的类型,带有特定键类型的键。 您可以为此使用任何键类型,尽管文字联合是最常见的(如果您使用 string
或 number
,那么映射类型的那部分变成......猜猜看? 一个索引签名!)接下来我将只使用文字的并集作为键集。
语法:映射类型的语法如下所示:
type Mapped<K extends keyof any> = [P in K]: SomeTypeFunction<P>;
type SomeTypeFunction<P extends keyof any> = [P]; // whatever
引入了一个新的类型变量P
,它遍历键组合in
和键集K
的每个成员。新类型变量仍在属性值SomeTypeFunction<P>
的范围内,即使它位于方括号之外。
整个对象类型:映射类型是整个对象类型。它不能与其他属性一起出现,也不能出现在界面中。这就像一个联合或交集类型:
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";
*/
可以使用任何密钥集:您不限于string
或number
。您可以使用任何一组string
文字或number
文字,甚至symbol
值(但使用起来更加复杂,所以我忽略了这一点)。您也可以在其中使用string
或number
,但当发生这种情况时,您会立即获得索引签名:
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<K, T>
utility type 本质上是相同的(它允许K extends string | number | symbol
),因此您可能希望使用它而不是您自己的。
好的,希望对您有所帮助;祝你好运!
Link to code
【讨论】:
【参考方案2】:您可以使用 typescript 的 Record<Key, Value>
实用程序代替索引签名。
interface MyObject<K extends string = string>
someProperty: Record<K, any>;
【讨论】:
以上是关于将泛型与索引类型相结合的主要内容,如果未能解决你的问题,请参考以下文章