Dustin Michels
Dustin Michels

Reputation: 3236

TypeScript: Describe Object of Objects

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

Answers (4)

Retsam
Retsam

Reputation: 33429

There's a few ways to go about this:

String Index Signature

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;
}

Mapped Type Syntax

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 Index Signature and Mapped Type 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)

Map objects

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

Karol Majewski
Karol Majewski

Reputation: 25810

Use the built-in Record type:

type StudentsById = Record<Student['id'], Student>;

Upvotes: 19

messerbill
messerbill

Reputation: 5639

you can simply make the key dynamic:

interface IStudentRecord {
   [key: string]: Student
}

Upvotes: 82

Ashifur rahman
Ashifur rahman

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

Related Questions