Closed mariusschulz closed 8 years ago
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!
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!!!
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' };
Not really, if you change your colors
to be:
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 ?
What 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: Example 1
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 TPropertyName
:
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 getNameFromValue
to return — the name of one of the keys or the numeric value on the right-hand side of your enum?
State
refers to the value of an enum,typeof State
would be the type of your enum (here: an object),keyof typeof State
is the key of your enum.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.
I've looked over this thread, and I'm a little confused. Why does this work:
type Point<D extends string> = {
[key in D]: number;
}
but this does not?
interface Point<D extends string> {
[key in D]: number
}
It seems to me that the two should be equivalent. What am I missing?
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
const point = {
x: 100,
y: 200
}
Then you'd have a type something like this:
interface IPoint {
x: number;
y: number;
}
type PointKeys = keyof IPoint;
But again maybe post a little more of what you're after here.
Or maybe you're after something like this:
interface IPoint {
x: number;
y: number;
}
const points = {
one: { x: 100, y: 200 },
two: { x: 200, y: 300 }
};
type PointKeys = keyof typeof points;
type Points = { [K in PointKeys]: IPoint };
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:
type Point<Keys extends string> = { [K in Keys]: number };
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
};
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 Record<T, K extends string>
if that makes sense.
I think index signature parameter should also allow for the String
type and sub-types because it is valid. I need this for the scenario I've explained here: https://github.com/microsoft/TypeScript/issues/6579#issuecomment-537306546
@mariusschulz, how about let keywords: { [key in keyof NodeType]: string }
?
@mariusschulz, how about
let keywords: { [key in keyof NodeType]: string }
?
I don't think it makes sense, keyof NodeType
will give you different literal strings - methods on String type.
What I tend to do is to reverse the problem, usually it's enough for my cases:
interface KeywordsMappings {
IfStatement: "if", // or string if you want to widen the type
WhileStatement: "while",
ForStatement: "for"
}
type Keywords = keyof KeywordsMappings
let keywords: KeywordsMappings = {
"IfStatement": "if",
"WhileStatement": "while",
"ForStatement": "for"
};
Not sure what happens but I guess it's a similar problem:
type TestMap<T extends string> = {[key in T]: string}
const a = <T extends string>(aa: T) => {
const x: TestMap<T> = {
[aa]: 'string'
}
}
//Type '{ [x: string]: string; }' is not assignable to type 'TestMap<T>'.(2322)
We can achieve this by using Record
:
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 Partial
:
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
}
}
}
https://github.com/microsoft/TypeScript/issues/5683#issuecomment-515744911
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 style
prop:
type CSSVariable = `--${string}`;
interface CSSProperties {
[index: CSSVariable]: any;
}
// ...
<div style={{ '--color-text': 'black' }}>{/* ... */}</div>
Same thing happens in Vue. Volar provides type checking in Vue templates, but the following template is currently an error:
<div :style="{ '--color-text': 'black' }" />
To fix this in a generic way we need to have the template literal type CSSVariable
in interface CSSProperties
as shown by @Flandre-X in previous comment.
According to https://github.com/Microsoft/TypeScript/pull/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