Reputation: 3236
Say I have an interface like this:
interface Student {
firstName: string;
lastName: string;
year: number;
id: number;
}
If I wanted to pass around an array of these objects I could simply write the type as Student[]
.
Instead of an array, I'm using an object where student ids are keys and students are values, for easy look-ups.
let student1: Student;
let student2: Student;
let students = {001: student1, 002: student2 }
Is there any way to describe this data structure as the type I am passing into or returning from functions?
I can define an interface like this:
interface StudentRecord {
id: number;
student: Student
}
But that still isn't the type I want. I need to indicate I have an object full of objects that look like this, the same way Student[]
indicates I have an array full of objects that look like this.
Upvotes: 40
Views: 48091
Reputation: 33429
There's a few ways to go about this:
As mentioned by @messerbill's answer you can use index signature for this:
interface StudentRecord {
[P: string]: Student;
}
Or if you want to be more cautious: (see caveat below)
interface StudentRecordSafe {
[P: string]: Student | undefined;
}
Alternatively, you can also use mapped type syntax:
type StudentRecord = {
[P in string]: Student;
}
or the more cautious version: (see caveat below)
type StudentRecordSafe = {
[P in String]?: Student
}
It's very similar to string index signature, but can use other things in replace of string
, such as a union of specific strings. There's also a utility type, Record
which is defined as:
type Record<K extends string, T> = {
[P in K]: T;
}
which means you can also write this as type StudentRecord = Record<string, Student>
. (Or type StudentRecordSafe = Partial<Record<string, Student>>
) (This is my usual preference, as it's IMO, easier to read and write Record than the long-hand index or type mapping syntax)
A caveat with both of these is that they're "optimistic" about the existence of students for a given id. They assume that for any string key, there's a corresponding Student
object, even when that's not the case: for example, this compiles for both:
const students: StudentRecord = {};
students["badId"].id // Runtime error: cannot read property id of undefind
Using the corresponding "cautious" versons:
const students: StudentRecordSafe = {}
students["badId"].id; // Compile error, object is potentially undefined
It's a bit more annoying to use, especially if you know that you'll only be looking up ids that exist, but it's definitely type safer.
As of version 4.1 Typescript now has a flag called noUncheckedIndexedAccess
which fixes this issue - any accesses to an index signature like this will now be considered potentially undefined when the flag is enabled. This makes the 'cautious' version unnecessary if the flag is on. (The flag is not included automatically by strict: true
and must be directly enabled in the tsconfig)
A slight code change, but a proper Map
object can be used, too, and it's always the "safe" version, where you have to properly check that thing`
type StudentMap = Map<string, Student>;
const students: StudentMap = new Map();
students.get("badId").id; // Compiler error, object might be undefined
Upvotes: 18
Reputation: 25810
Use the built-in Record
type:
type StudentsById = Record<Student['id'], Student>;
Upvotes: 19
Reputation: 5639
you can simply make the key dynamic:
interface IStudentRecord {
[key: string]: Student
}
Upvotes: 82
Reputation: 72
instate of let students = {001: student1, 002: student2 } you could just say let students = {student1, student2 } then access them by their index like students[0] and students[1] and if you need info out of the student you can do that like students[0].firstName
Upvotes: -9