最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is it possible for TypeScript to infer keys from a dynamic object? - Stack Overflow

programmeradmin3浏览0评论

What I'm trying to achieve here is intellisense/autoplete for an object that's been generated from an array - something like an Action Creator for Redux, an array of strings (string[]) that can be reduced to an object with shape { [string]: string }.

For example:

const a = ['ONE', 'TWO', 'THREE'];

const b = a.reduce((acc, type) => ({ ...acc, [type]: type }), {});

console.log(b);
// Output: b = { ONE: 'ONE', TWO: 'TWO', THREE: 'THREE' };

I've managed to get TypeScript to understand that much, using the below. TypeScript understands that the keys are strings, but does not know what they are.

interface ITypesReturnObject { [s: string]: string }

Has anyone worked out a way to inform TypeScript that the keys on the object are equal to the strings in the array?

Any help would be greatly appreciated.

What I'm trying to achieve here is intellisense/autoplete for an object that's been generated from an array - something like an Action Creator for Redux, an array of strings (string[]) that can be reduced to an object with shape { [string]: string }.

For example:

const a = ['ONE', 'TWO', 'THREE'];

const b = a.reduce((acc, type) => ({ ...acc, [type]: type }), {});

console.log(b);
// Output: b = { ONE: 'ONE', TWO: 'TWO', THREE: 'THREE' };

I've managed to get TypeScript to understand that much, using the below. TypeScript understands that the keys are strings, but does not know what they are.

interface ITypesReturnObject { [s: string]: string }

Has anyone worked out a way to inform TypeScript that the keys on the object are equal to the strings in the array?

Any help would be greatly appreciated.

Share Improve this question edited Oct 26, 2018 at 11:39 Brody McKee asked Oct 26, 2018 at 10:47 Brody McKeeBrody McKee 2033 silver badges5 bronze badges 5
  • Is the ment in your snippet the expected output or the wrong output? Can you pleas elaborate what you have and what is the expected output? – Cristian S. Commented Oct 26, 2018 at 10:51
  • Possible duplicate of JavaScript to TypeScript: Intellisense and dynamic members – softbear Commented Oct 26, 2018 at 10:58
  • @CristianS. The ment is current output, which is fine. The question is around calling that object - when I enter b. I don't get a list of keys in intellisense/autoplete, so the user of this object has no way to know what options are available. – Brody McKee Commented Oct 26, 2018 at 11:41
  • @softbear Thanks for that. It looks like a similar problem to what I'm trying to achieve, but I'm not sure I know how to make the solution work in this scenario. If you do, that would be greatly appreciated. – Brody McKee Commented Oct 26, 2018 at 11:44
  • 1 Aha! Interesting. IntelliSense works with new DynamicObject({id:1}).id but doesn't work with d = {id:1};new DynamicObject(d) which is what you need. Hmm... head scratcher for sure. – softbear Commented Oct 26, 2018 at 12:16
Add a ment  | 

3 Answers 3

Reset to default 10

(Assuming you use TS3.0 or greater in the following)

TypeScript supports the concept of string literal types as well as tuple types, so it is possible to get your value a to have the type ['ONE', 'TWO', 'THREE'] as well as the value ['ONE', 'TWO', 'THREE'], like this:

const a: ['ONE', 'TWO', 'THREE'] = ['ONE', 'TWO', 'THREE'];

(A less redundant way to get this to happen will e later):

Then you can represent the intended type of b as a mapping from keys to values that match the key exactly, using a mapped type:

type SameValueAsKeys<KS extends string[]> = { [K in KS[number]]: K };

which could be used like this:

const b: SameValueAsKeys<typeof a> = a.reduce((acc, type) => ({ ...acc, [type]: type }), {} as any);
b.ONE; // property type is "ONE"
b.TWO; // property type is "TWO"
b.THREE; // property type is "THREE"

Notice how the piler knows that b has three keys, "ONE", "TWO", and "THREE", and that the values are the same as the keys.


So TypeScript certainly supports this type of dynamic typing. Unfortunately it's a bit tedious to use as I showed it above. One way to make this less annoying is to add some helper functions that allow the piler to infer the proper types, or at least hide the type assertions in a library where the developer won't have to worry about them.

First, for a... the piler doesn't infer tuple types, and it also tends to widen string literals to the string type except in certain instances. Let's introduce a helper function named stringTuple():

function stringTuple<T extends string[]>(...args: T) { return args; }

This infers a tuple type from rest arguments. Now we can use it to make a without typing redundant string values:

const a = stringTuple("ONE", "TWO", "THREE");

Next, let's introduce a function which takes a list of strings and returns an object whose keys are those strings and whose values match the strings:

function keyArrayToSameValueAsKeys<T extends string[]>(keys: T): SameValueAsKeys<T>;
function keyArrayToSameValueAsKeys(keys: string[]): { [k: string]: string } {
  return keys.reduce((acc, type) => ({ ...acc, [type]: type }), {});
}

Here we are using your same code with reduce, but we're hiding it inside its own function and using a single overload call signature to represent the intended output type. Now, we can get b like this:

const b = keyArrayToSameValueAsKeys(a);
b.ONE; // property type is "ONE"
b.TWO; // property type is "TWO"
b.THREE; // property type is "THREE"

If you put stringTuple() and keyArrayToSameValueAsKeys() in a library, the user can use them without too much trouble:

const otherObject = keyArrayToSameValueAsKeys(stringTuple("x", "y", "z"));
// const otherObject: {X: "X", Y: "Y", Z: "Z"}

Or you can merge them together like this:

function keysToSameValueAsKeys<T extends string[]>(...keys: T): { [K in T[number]]: K };
function keysToSameValueAsKeys(keys: string[]): { [k: string]: string } {
  return keys.reduce((acc, type) => ({ ...acc, [type]: type }), {});
}

And then get the output in one call, like this:

const lastOne = keysToSameValueAsKeys("tic", "tac", "toe");
// const lastOne: {tic: "tic", tac: "tac", toe: "toe"};

Okay, hope that's of some help. Good luck!

If you know in advance what will be in the array you can do something like this:

type myType = 'ONE' | 'TWO' | 'THREE';
const a: myType[] = ['ONE', 'TWO', 'THREE'];

const b: { [key in myType]: myType } = a.reduce<{ [key in myType]: myType }>((acc, type) => ({ ...acc, [type]: type }), <any>{});

// correct intellisense
b.ONE === 'ONE'

Came here looking for a better answer. Here's what I came up with and it works great:

const makeStyles = <T extends string = string>(styles: Record<T, string>): Record<T, string> => {
  return styles;
}

const classes = makeStyles({
  hello: 'hello world',
  another: 'something else'
})

/** 
 * now typescript will infer the keynames that appear in the object passed 
 * to makeStyles
 */
console.log(classes)


发布评论

评论列表(0)

  1. 暂无评论