40

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.

3
  • 4
    something like this: let students: {[key: string]: Student} = .... Also, object keys cannot be numbers (but can be numeric strings). Commented Dec 11, 2018 at 22:30
  • 3
    Your only options here are using the answer @GetOffMyLawn suggested or alternatively using a Map - which is definitely a good fit! The type for that would be Map<number, Student>. You should note that { 1234: foo } implicitly coerces '1234' to a string such that { "1234": foo } and { 1234: foo } are equivalent. If you need to maintain numbers as keys you must use a Map. Commented Dec 11, 2018 at 22:35
  • 2
    typescriptlang.org/docs/handbook/… Commented Dec 11, 2018 at 22:35

4 Answers 4

83

you can simply make the key dynamic:

interface IStudentRecord {
   [key: string]: Student
}
Sign up to request clarification or add additional context in comments.

2 Comments

@DustinMichels if it's the correct answer, please mark the answer as accepted by pressing the green tick below the voting options on this answer :)
What messerbill is calling "dynamic" is generally referred to as index signature (typescriptlang.org/docs/handbook/2/…).
19

Use the built-in Record type:

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

4 Comments

Is there an advantage to using a Record over the indexable types solution?
Record is aware of your keyofStringsOnly setting. If it's turned on, it will only allow the keys of type string (which is what JavaScript does to keys in runtime).
This is way more TS way :D
dot notation is not working for this.
18

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

Comments

-9

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

1 Comment

This isn't what the OP is asking

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.