There are several approaches you can take:
String Index Signature
You can utilize an index signature, as mentioned in @messerbill's response:
interface StudentRecord {
[P: string]: Student;
}
If you prefer a more cautious approach (see caveat below):
interface StudentRecordSafe {
[P: string]: Student | undefined;
}
Mapped Type Syntax
Another option is to use the mapped type syntax:
type StudentRecord = {
[P in string]: Student;
}
For a safer version of this method (see caveat below):
type StudentRecordSafe = {
[P in String]?: Student
}
This approach is similar to a string index signature but allows for variations like using a union of specific strings. You can also make use of the utility type called Record
, defined as follows:
type Record<K extends string, T> = {
[P in K]: T;
}
Therefore, you could write it as
type StudentRecord = Record<string, Student>
. (Or
type StudentRecordSafe = Partial<Record<string, Student>>
) (Personally, I find using Record easier to understand and implement compared to the longer syntax)
A Caveat with Index Signature and Mapped Type Syntax
An important point to note with both these methods is their optimistic assumption regarding the existence of students for a given id. They assume that every string key corresponds to a valid Student
object, even when that may not be true. For instance, the following code compiles successfully for both cases:
const students: StudentRecord = {};
students["badId"].id // Runtime error: cannot read property id of undefind
Comparatively, when using the cautious versions:
const students: StudentRecordSafe = {}
students["badId"].id; // Compile error, object might be undefined
Although slightly more tedious to work with, especially if you're certain about the existing ids, it does offer better type safety.
With the release of version 4.1, Typescript now includes a flag named noUncheckedIndexedAccess
that addresses this issue. With the flag enabled, any accesses to an index signature like this will be considered potentially undefined. This renders the "cautious" version unnecessary if the flag is activated. (Please note that the flag is not automatically included with strict: true
and needs to be manually enabled in the tsconfig file)
Map objects
A slight alteration in your code, but you can also opt for a proper Map
object, which inherently provides the "safe" version where thorough checks are required before accessing elements:
type StudentMap = Map<string, Student>;
const students: StudentMap = new Map();
students.get("badId").id; // Compiler error, object might be undefined