1

The problem is that I want to use a new type as key for a dictionary but I want to dynamically generate the entries using a loop. But I will prefer not to use ? operator because I know I'll fill all keys and I don't want to force ! evaluation for each call.

const studentList = [
  A,
  B,
  C,
// many many students
] as const;
type Students = typeof studentList[number];

// class Info(); // some properties like grades and teacher

const studentInfo: { [key in Students]: Info }; // won't work const must be initialized
const studentInfo: { [key in Students]: Info } = {}; // won't work as all keys are not defined
const studentInfo: { [key in Students]?: Info } = {}; // I want to avoid ?

for (let name of Students) {
  // get info from some db
  { grade, teacher } = lookUp(name)

  // initialize dictionary keys
  studentInfo[name] = Info(grade, teacher);
}

studentInfo.A!.name; // I want to avoid !

Is there anyway to use the key itself to generate the dictionary so that the type signatures checkout. Maybe something like const studentInfo = { key for key in Students | .... };. And the reason I feel this should work out is because the compiler can reason that all the keys of the type have been used to create the dictionary.

Is this possible in some way?

I previously asked a version of this question with enums, but thanks to this answer I realized a new type might be better and hit this new question :).

twitu
  • 553
  • 6
  • 12

1 Answers1

2

This is the best I can come up with at the moment:

const students = ['A', 'B', 'C'] as const;

class Info {
    name = '';
}

type StudentInfo = { [key in typeof students[number]]: Info; };

const studentInfo = students.reduce((a, v) => ({ ...a, [v]: new Info() }), {})
                      as StudentInfo;

studentInfo.A.name;

The new Info() part is where you would do your database lookup and create the actual Info object.

Ideally, there should be a way to avoid that cast when creating the studentInfo object as well, but again the problem here is bridging runtime objects to compile-time constructs.

The compile time StudentInfo type is correctly defined, but TypeScript is unable to infer that the result of my reduce() operation conforms to that type. I thought I might have better luck using the ES2019 Object.fromEntries() method, but the compiler still needs the explicit cast:

const studentInfo = Object.fromEntries(students.map(s => [s, new Info()]))
                      as StudentInfo;

Update
I asked a new question to see if there was a way to get around having to do an explicit type assertion and got some good answers. The cast can be avoided by using Object.create(null) instead of {} to initalize the result of the reduce() operation:

const studentInfo = students.reduce((a, v) => ({ ...a, [v]: new Info() }),
                      Object.create(null));

There are however some caveats. Check the linked question for details.

Robby Cornelissen
  • 91,784
  • 22
  • 134
  • 156
  • Wow! This is a pretty neat solution, I have a lot of typescript to learn. In the interest of a complete answer, would you like to add if explicit type casting is frowned upon? and secondly if there any considerations for using ES2019 or higher? – twitu Dec 28 '20 at 10:02
  • 1
    I don't casting is necessarily frowned upon, but what you're basically doing is saying to the compiler is "I know you can't figure this out, so I figured it out for you". The risk, of course, is that you've figured it out incorrectly, and that the compiler has to act based on incorrect information. For the second question, see [here](https://stackoverflow.com/questions/51043439/using-latest-javascript-features-in-typescript-such-as-es2018). – Robby Cornelissen Dec 28 '20 at 10:40
  • @twitu I asked a [new question](https://stackoverflow.com/questions/65476787/how-to-dynamically-create-an-object-based-on-a-readonly-tuple-in-typescript) to see if it was possible to do this without a cast, and got some good answers. Unfortunately, it seems that none of the solutions are particularly elegant. – Robby Cornelissen Dec 29 '20 at 00:57