'Combining generics with index type
Since this works:
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 };
but compiler reports on _:
An index signature parameter type must be 'string' or 'number'.
Changing Key extends string to Key extends string | number does nothing (same error).
What is the reason why it fails and how would look a correct solution? (Preferably without using Any and similar.)
Edit1:
type XY = 'x' | 'y';
const myXY: XY = 'x';
const myString: string = myXY;
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).
Solution 1:[1]
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
Syntax: both index signatures and mapped types use bracketed keylike notation within an object type, as in
{[Some Key-like Expression]: T}
Now for the differences:
INDEX SIGNATURES
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, these key type can only be exactly string, number, or symbol, or a "pattern template literal" types as implemented in ms/TS#40598 like `foo_${string}`, or a union of these.
Syntax: The syntax for an index signature looks like this:
type StringIndex<T> = {[dummyKeyName: string]: T} type NumberIndex<T> = {[dummyKeyName: number]: T}There is a dummy key name (
dummyKeyNameabove) which can be whatever you want and does not have any meaning outside the brackets, followed by a type annotation (:) of eitherstringornumber.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 }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
stringornumberaside fromProxyobjects). 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 toundefined), even with--strictNullChecksenabled, 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 runtimeProperties 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.Only
string,number,symbol, or a pattern template literal is allowed as the key type: you can use astringindex signature to represent a dictionary-like type, or anumberindex signature to represent an array-like type. TypeScript 4.4 introduced support forsymboland pattern template literals, and unions of these.
You can't narrow the index signature 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 that no member of an index signature parameter type can be a "singleton" or "unit" literal type.
MAPPED TYPES
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]; // whateverA new type variable
Pis introduced, which iterates over each member of the union of keysinthe key setK. The new type variable is still in scope in the property valueSomeTypeFunction<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 propsProperty 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"; } */Any key set may be used: you are not restricted to
string,number,symbolor pattern template literals. You can use any set ofstringliterals ornumberliterals. You can also usestringornumberin 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 -> stringAnd since any key set may be used, it can be generic:
type MyRecord<Key extends string, Value> = { [P in Key]: Value };
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.
Solution 2:[2]
You can use the typescript's Record<Key, Value> utility instead of the index signature.
interface MyObject<K extends string = string> {
someProperty: Record<K, any>;
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Vahid |
