将泛型与索引类型相结合 [英] Combining generics with index type
问题描述
既然这样有效:
const f = <T extends string>(x: T) => x;
f("");
interface Dictionary<T> { [key: string]: T; }
const dict: Dictionary<number> = { a: 1 };
我希望以下代码也能正常工作:
I was expecting following code to work as well:
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
什么都不做(同样的错误).
Changing Key extends string
to Key extends string | number
does nothing (same error).
它失败的原因是什么?如何看起来是正确的解决方案?(最好不使用 Any
和类似的.)
What is the reason why it fails and how would look a correct solution? (Preferably without using Any
and similar.)
编辑 1:
type XY = 'x' | 'y';
const myXY: XY = 'x';
const myString: string = myXY;
由于这有效,我假设索引类型也同样成立(string
的子集可以充当索引类型所需的 string
的角色).>
Since this works, I was assuming same holds with indexed types (subset of string
can pose in a role of string
which is required by indexed type).
推荐答案
来说说索引签名类型 和映射类型一>.它们具有相似的语法并做类似的事情,但它们并不相同.以下是相似之处:
Let's talk about index signature types and mapped types. They have similar syntax and do similar-ish things, but they're not the same. Here are the similarities:
它们都是表示一系列属性的对象类型
They are both object types representing a range of properties
语法:索引签名和映射类型都在对象类型中使用括号中的类键表示法,如{[Some Key-like Expression]:T}
Syntax: both index signatures and mapped types use bracketed keylike notation within an object type, as in {[Some Key-like Expression]: T}
现在的差异:
索引签名描述对象类型或接口的一部分,表示任意数量相同类型的属性,带有来自某个特定对象的键键类型.目前,此密钥类型只有两种选择:string
或 number
.
Index signatures describe part of an object type or interface representing an arbitrary number of properties of the same type, with keys from a certain key type. Currently, there are only two choices for this key type: string
, or number
.
语法:索引签名的语法如下所示:
Syntax: The syntax for an index signature looks like this:
type StringIndex<T> = {[dummyKeyName: string]: T}
type NumberIndex<T> = {[dummyKeyName: number]: T}
有一个虚拟键名(上面的dummyKeyName
)可以是任何你想要的,括号外没有任何意义,后面跟着一个类型注释(:
) 的 string
或 number
.
There is a dummy key name (dummyKeyName
above) which can be whatever you want and does not have any meaning outside the brackets, followed by a type annotation (:
) of either string
or number
.
对象类型的一部分:索引签名可以与对象类型或接口中的其他属性一起出现:
Part of an object type: an index signature can appear alongside other properties in an object type or interface:
interface Foo {
a: "a",
[k: string]: string
}
任意数量的属性:一个可索引类型的对象不需要为每个可能的键都拥有一个属性(这对于 string
或 甚至是不可能做到的)编号
除了Proxy代码>
对象).相反,您可以将包含任意数量的此类属性的对象分配给可索引类型.请注意,当您从可索引类型读取属性时,编译器将假定该属性存在(而不是 undefined
),即使启用了 --strictNullChecks
,
Arbitrary number of properties: an object of an indexable type is not required to have a property for every possible key (which is not even really possible to do for string
or number
aside from Proxy
objects). Instead, you can assign an object containing an arbitrary number of such properties to an indexable type. Note that when you read a property from an indexable type, the compiler will assume the property is present (as opposed to undefined
), even with --strictNullChecks
enabled, even though this is not strictly type safe. Example:
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"}
的类型,你不能用索引签名来做到这一点.
Properties of the same type: all of the properties in an index signature must be of the same type; the type cannot be a function of the specific key. So "an object whose property values are the same as their keys" cannot be represented with an index signature as anything more specific than {[k: string]: string}
. If you want a type that accepts {a: "a"}
but rejects {b: "c"}
, you can't do that with an index signature.
仅允许 string
或 number
作为键类型:目前您可以使用 string
索引签名来表示字典-like 类型,或 number
索引签名来表示类似数组的类型.就是这样.您不能将类型扩展为 string |number
或将其缩小为一组特定的 string
或 number
文字,例如 "a"|"b"
或 1|2
.(关于为什么它应该接受更窄的集合的推理是合理的,但这不是它的工作原理.规则是索引签名参数类型必须 be string
或 number
".)有一些工作正在放宽此限制并允许任意键类型在索引签名中,请参阅 microsoft/TypeScript#26797,但似乎是 停滞至少现在是这样.
Only string
or number
is allowed as the key type: currently you can use a string
index signature to represent a dictionary-like type, or a number
index signature to represent an array-like type. That's it. You can't widen the type to string | number
or narrow it to a particular set of string
or number
literals like "a"|"b"
or 1|2
. (Your reasoning about why it should accept a narrower set is plausible but that's not how it works. The rule is "an index signature parameter type must be string
or number
".) There was some work being done to relax this restriction and allow arbitrary key types in an index signature, see microsoft/TypeScript#26797, but seems to be stalled at least for now.
另一方面,映射类型描述了一个完整的对象类型,而不是一个接口,代表了一个特定的属性集可能变化的类型,使用来自特定密钥类型的密钥.您可以为此使用任何键类型,尽管最常见的是文字联合(如果您使用 string
或 number
,则该部分的映射类型变成了...猜猜怎么着?索引签名!)接下来我将只使用文字的联合作为键集.
A mapped type on the other hand describes an entire object type, not an interface, representing a particular set of properties of possibly varying types, with keys from a certain key type. You can use any key type for this, although a union of literals is most common (if you use string
or number
, then that part of the mapped type turns into... guess what? an index signature!) In what follows I will use only a union of literals as the key set.
语法:映射类型的语法如下所示:
Syntax: The syntax for a mapped type looks like this:
type Mapped<K extends keyof any> = {[P in K]: SomeTypeFunction<P>};
type SomeTypeFunction<P extends keyof any> = [P]; // whatever
引入了一个新的类型变量P
,它遍历键集K
中in
键联合的每个成员.新类型变量仍然在属性值 SomeTypeFunction
的范围内,即使它在括号之外.
A new type variable P
is introduced, which iterates over each member of the union of keys in
the key set K
. The new type variable is still in scope in the property value SomeTypeFunction<P>
, even though it's outside the brackets.
整个对象类型:映射类型是整个对象类型.它不能与其他属性一起出现,也不能出现在界面中.它就像一个联合或交叉类型:
An entire object type: a mapped type is the entire object type. It cannot appear alongside other properties and cannot appear in an interface. It's like a union or intersection type in that way:
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
}
一组特定的属性:与索引签名不同,映射类型在键集中的每个键必须恰好有一个属性.(例外情况是,如果该属性恰好是可选的,要么是因为它是从具有可选属性的类型映射而来的,要么是因为您使用 ?
修饰符将该属性修改为可选的):
A particular set of properties: unlike index signatures, a mapped type must have exactly one property per key in the key set. (An exception to this is if the property happens to be optional, either because it's mapped from a type with optional properties, or because you modify the property to be optional with the ?
modifier):
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
属性类型可能会有所不同:因为迭代键类型参数在属性类型的范围内,您可以将属性类型作为键的函数进行更改,如下所示:
Property types may vary: because the iterating key type parameter is in scope in the property type, you can vary the property type as a function of the key, like this:
type SameName = { [K in "foo" | "bar" | "baz"]: K };
/* type SameName = {
foo: "foo";
bar: "bar";
baz: "baz";
} */
可以使用任何键集:您不限于string
或number
.您可以使用任何一组 string
文字或 number
文字,甚至 symbol
值(但这些值使用起来更加复杂,所以我无视).您也可以在其中使用 string
或 number
,但在发生这种情况时您会立即获得索引签名:
Any key set may be used: you are not restricted to string
or number
. You can use any set of string
literals or number
literals, and even symbol
values (but those are even more complicated to use so I'm ignoring that). You can also use string
or number
in there, but you immediately get an index signature when that happens:
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
而且由于可以使用任何键集,因此它可以是通用的:
And since any key set may be used, it can be generic:
type MyRecord<Key extends string, Value> = { [P in Key]: Value };
这就是您制作 MyRecord
的方式.它不能是可索引的类型;只有一个映射类型.并注意内置Record
实用程序类型 本质上是相同的(它允许 K 扩展字符串 | 数字 | 符号
),因此您可能希望使用它而不是您自己的.
So that's how you would make MyRecord
. It can't be an indexable type; only a mapped type. And note that the built-in Record<K, T>
utility type is essentially the same (it allows K extends string | number | symbol
), so you might want to use that instead of your own.
好的,希望有帮助;祝你好运!
Okay, hope that helps; good luck!
这篇关于将泛型与索引类型相结合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!