-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
String literal types as index signature parameter types? #5683
Comments
This issue is kind of the "dual" of #2491, and if we took this on we might want to reconsider that issue. This is something that @weswigham has brought up a few times with me. One of the issues with this sort of thing is that |
I'd like to argue from another perspective to point out why I think this feature is worth implementing. Consider the above I agree, the issues are related. Let's see if we can find a solution here. |
For that sort of thing, you don't necessarily need a string literal index signature - an alternative approach is to just use the appropriate property name when indexing with string literal types. It's one of the open questions on #5185:
|
Dup of #2491 |
+1 |
There's a highly relevant discussion on this at the exact duplicate issue #7656. I suggest checking out some of the information there. |
In order to truly implement something like this, the index signature parameter would first need to be type checked against the specialized type they are constrained to, e.g. |
This comment in #7660 is highly relevant to the topic here, though refers to the more general issue of how strictly should index signature keys be type-checked. |
+1 |
@christyharagan posted some more motivating examples in #8336. As @malibuzios points out, |
Adding some more examples to what @mariusschulz was saying
The ability to enumerate the set of possible string values would be very useful. For example, given: type DogName = "spike" | "brodie" | "buttercup";
interface DogMap {
[index: DogName ]: Dog
}
let dogs: DogMap = { ... }; ...it would be really nice to be able to do: let dogNames: DogName[] = Object.keys(dogs);
// or
for (let dogName:DogName in dogs) {
dogs[dogName].bark();
} |
Here is another example of how it would be nicer if we were able to use string literal types as index keys. Consider this interface: interface IPerson {
getFullName(): string;
} If your tests this, you might write something like this: let person: IPerson = jasmine.createSpyObj('person', ['getFullName']); or let person = jasmine.createSpyObj<IPerson>('person', ['getFullName']); but suppose you messed up and instead wrote this: let person: IPerson = jasmine.createSpyObj('person', ['foo']); Currently there is no way for TS to let you know there is a problem. But if the type definition could look something like this: function createSpyObj<T extends string>(baseName: string, methodNames: T[]): {[key: T]: any}; then the inferred type could be something more like Now, I'm not sure right now if the compiler would infer the type of T as a string literal union type for something like this: let person: IPerson = jasmine.createSpyObj('person', ['foo', 'bar']); //ERROR Ideally there would be a way to help TS infer the generic |
+1 |
Make the boolean field an optional parameter, due to the lack of typing possible for the `key` (cf. microsoft/TypeScript#5683) at the moment.
A related question, I wonder why: type NodeType = "IfStatement"
| "WhileStatement"
| "ForStatement"; The code below works: type GoodStuff = {[P in NodeType]?: string } // Note that value type is set to "string"
let keywords: GoodStuff = {
IfStatement: "if",
WhileStatement: "while",
another: "another", // This triggers error as expected
}; But this doesn't: type GoodStuff = {[P in NodeType]?: NodeType } // Note that value type is set to "NodeType"
let keywords: GoodStuff = {
IfStatement: "if",
WhileStatement: "while",
another: "another", // This no longer triggers error :(
}; |
@benjamin21st It does seem to work correctly. See TypeScript playground with TypeScript |
@karol-majewski Ah~ Thanks! Then I'll just have to convince my team to upgrade to |
If you have an enum (indexed by numbers), but you want your object to have string keys, here’s my solution: enum NodeEnum {
IfStatement,
WhileStatement,
ForStatement,
}
const keywords: { [index in keyof typeof NodeEnum]: string } = {
"IfStatement": "if",
"WhileStatement": "while",
"ForStatement": "for",
} |
Actually you can write this like the following: enum Color {
red,
green,
blue
}
type ColorMap<C extends object, T> = { [P in keyof C]: T };
const colors: ColorMap<typeof Color, string> = {
red: 'red',
green: 'green',
blue: 'blue'
}; Boom type checking! |
Thanks!!! This is AWESOME!!! |
Not really, if you change your const colors: ColorMap<typeof Color, string> = {
red: 'green', // <------- We don't expect this, do we?
green: 'green',
blue: 'blue'
}; |
Same situation type Method = 'get' | 'post' | 'put' | 'delete'
const methods: Method[] = ['get', 'post', 'put', 'delete']
class Api {
[method in Method]: Function // <-- Error here
constructor() {
methods.forEach(method => {
this[method] = (options: any) => { this.send({ ...options, method }) }
})
}
send(options:any) {
// ....
}
} How should i handle this case ? |
For Googlers and othersWhat you want (and does NOT work): type Ma = { [key: 'Foo']: number }
type Mb = { [key: 'Foo' | 'Bar']: number }
type Mc = { [key: 'Foo' | 'Bar' | 0.3]: number }
// etc What you need (and does work): type Ma = { [key in 'Foo']?: number }
type Mb = { [key in 'Foo' | 'Bar']?: number }
type Mc = { [key in 'Foo' | 'Bar' | 0.3]?: number }
const x: Ma = {}
const y: Ma = { 'Foo': 1 }
const z: Mc = { [0.3]: 1 }
// const w: Ma = { 'boop': 1 } // error Unfortunate constraints: type Mx = {
Bar: number // OK, but has to be a number type
[key: string]: number
} Example 2 type My = {
Bar: number, // Error, because of below
[key in 'Foo']: number
} |
Is it possible to achieve something like this: // TPropertyName must be a string
export type Foo<TPropertyName = "propertyName"> = {
[key in TPropertyName]: number
}; |
@n1ru4l You need to set a constraint on export type Foo<TPropertyName extends string = "propertyName"> = {
[key in TPropertyName]: number
}; |
Ok, with all these solutions proposed, it seems there is still none piece of the puzzle missing; iterating over an object's keys: declare enum State {
sleep,
idle,
busy,
}
type States = { [S in keyof typeof State]: number };
const states: States = {
sleep: 0x00,
idle: 0x02,
busy: 0x03,
};
function getNameFromValue(state: number): State | undefined {
for (const k in states){
if (states[k] === state) {
return k; // type string !== type State and cannot be typecasted
}
}
} The solution @dcousens proposed doesn't really help because my State enum is actually 20 lines of code and I don't think anyone would want that in their project. |
@LukasBombach What do you want
If your enum looks like this: enum State {
sleep = 0x00,
idle = 0x02,
busy = 0x03,
} Then you can get the key by doing: function getNameFromValue(state: number): keyof typeof State | undefined {
return State[state] as keyof typeof State | undefined;
} and the value by doing: function getNameFromValue(state: number): State | undefined {
for (const k of UNSAFE_values(State)) {
if (state === k) {
return k
}
}
return undefined;
}
const UNSAFE_values = <T extends object>(source: T): T[keyof T][] =>
Object.values(source) as T[keyof T][]; |
@karol-majewski thank you! What I want to return is the String that is restricted to specific values, I managed to do it the way I do it up there. The way I understand your solution, it is similar to mine but the keys and values / the access to it is reversed. What bugs me is that I have to do a type cast, which I'd like to avoid. |
Perhaps you could post an example of what you're after here as what you're showing here is wanting each key in a string. Not typical. If you have a point that has say x and y
Then you'd have a type something like this:
But again maybe post a little more of what you're after here. |
Or maybe you're after something like this:
|
What I've been trying to express is a Point with an arbitrary number of named dimensions. For example: const point2D: Point<'x' | 'y'> = {x: 2, y: 4};
const point6D: Point<'u' | 'v' | 'w' | 'x' | 'y' | 'z'> = {
u: 0,
v: 1,
w: 2,
x: 3,
y: 4,
z: 5
}; But I think my use case isn't as important as the question of why the index signature works in a type alias but not in an interface? I just spent a long time trying to get it to work as an interface before realizing that the same thing as a type alias works. It's a little confusing why one would work but not the other. |
I see, I misunderstood you're not asking for a solution but the why? So this works just fine, I'm assuming you realized that but to be clear:
Unlike the type alias which is enumerating the keys an interface is a definition hence the generic type would have to be an object or a Symbol. So what you're trying to do here needs to be done with a type alias as you're not defining it but rather representing what it is based on the keys. Think of it like a |
I think index signature parameter should also allow for the |
@mariusschulz, how about |
I don't think it makes sense, What I tend to do is to reverse the problem, usually it's enough for my cases:
|
Not sure what happens but I guess it's a similar problem:
|
We can achieve this by using type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Record<NodeType, string>
// works!
var myNodeTypeObject: NodeTypeObject = {
IfStatement: 'if',
WhileStatement: 'while',
ForStatement: 'for',
}
// complains if there are missing proeprties
var myNodeTypeObject: NodeTypeObject = {
IfStatement: 'if',
WhileStatement: 'while',
} // --> Error : Property 'ForStatement' is missing but required by type 'Record<NodeType, string>'.
// Complains if additional properties are found
var myNodeTypeObject: NodeTypeObject = {
IfStatement: 'if',
WhileStatement: 'while',
ForStatement: 'for',
foo :'bar' // --> Error : 'foo' does not exist in type 'Record<NodeType, string>'.
} Bonus: If we want the properties to be optional we can do it by using type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Partial<Record<NodeType, string>>
// works!
var myNodeTypeObject: NodeTypeObject = {
IfStatement: 'if',
WhileStatement: 'while',
} try it in the typescript playground |
But in this solution your cannot iterate the keys: type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Record<NodeType, string>
// works
var myNodeTypeObject: NodeTypeObject = {
IfStatement: 'if',
WhileStatement: 'while',
ForStatement: 'for',
}
function getNameFromValue(str: string): NodeType | undefined {
for (const k in myNodeTypeObject){
if (myNodeTypeObject[k] === str) { // any
return k; // is a string
}
}
} |
If we allow template literal types to be used as index signature parameter types, then we could do something like this to let CSS variables be assignable to the React type CSSVariable = `--${string}`;
interface CSSProperties {
[index: CSSVariable]: any;
}
// ...
<div style={{ '--color-text': 'black' }}>{/* ... */}</div> |
Same thing happens in Vue. <div :style="{ '--color-text': 'black' }" /> To fix this in a generic way we need to have the template literal type |
According to #5185, string literal types are assignable to plain strings. Given the following type definition:
Both these assignments are valid:
However, string literal types currently can't be used as index signature parameter types. Therefore, the compiler complains about the following code:
Shouldn't that scenario be supported, given that string literal types are assignable to strings?
/cc @DanielRosenwasser
The text was updated successfully, but these errors were encountered: