TypeScript: Describe Object of Objects

16,728

Solution 1

you can simply make the key dynamic:

interface IStudentRecord {
   [key: string]: Student
}

Solution 2

Use the built-in Record type:

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

Solution 3

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
Share:
16,728
Dustin Michels
Author by

Dustin Michels

Graduated from Carleton College in June 2018 with a BA in Computer Science. I like writing useful stuff, especially with Python and JavaScript. I like data visualization and creating tools for data analytics. Folk guitarist, utilitarian cyclist, environmentalist.

Updated on June 22, 2022

Comments

  • Dustin Michels
    Dustin Michels almost 2 years

    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.