-
-
Save FlandreDaisuki/99ca925e5ea986141ae932d29940714b to your computer and use it in GitHub Desktop.
Records and dictionaries in TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
In JavaScript, objects can be used to serve various purposes. | |
To maximise our usage of the type system, we should assign different types to our objects depending | |
on the desired purpose. | |
In this blog post I will clarify two common purposes for objects known as records and dictionaries | |
(aka maps), and how they can both be used with regards to the type system. | |
*/ | |
// | |
// # Dictionary/Map type | |
// - Keys are unknown, for example a dictionary of unknown user IDs (strings) to usernames. | |
// - All key lookups should be valid | |
// - Described using index signature types | |
// - Index signature keys can be strings or numbers. (JavaScript coerces numbers to strings at | |
// runtime.) | |
// - Optionally, index signature value can include `undefined` (i.e. to model keys that may not | |
// exist). | |
// - Also see JavaScript's built in `Map` type | |
// | |
{ | |
// String index signature | |
const dictionary: { [userId: string]: string } = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string = dictionary.a | |
const b: string = dictionary['b'] | |
const x: string = dictionary.x | |
const z: string = dictionary['z'] | |
} | |
{ | |
// String index signature including undefined (for increased safety) | |
const dictionary: { [userId: string]: string | undefined } = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string | undefined = dictionary.a | |
const b: string | undefined = dictionary['b'] | |
const x: string | undefined = dictionary.x | |
const z: string | undefined = dictionary['z'] | |
} | |
{ | |
// Number index signature | |
const dictionary: { [userId: number]: string | undefined } = { | |
0: 'foo', | |
1: 'bar', | |
2: 'baz', | |
} | |
const a: string | undefined = dictionary[0] | |
const b: string | undefined = dictionary[100] | |
// Error: Element implicitly has an 'any' type because index expression is not of type 'number'. | |
const c: string | undefined = dictionary['200'] | |
} | |
// | |
// # Record type | |
// - Keys are known, for example a record of known user IDs (`a` and `b`) and their usernames. | |
// - Unknown key lookups should be invalid | |
// - Described using interfaces (defined manually or via mapped types) | |
// | |
// Note: TypeScript handles unknown key lookups differently depending on the notation used: | |
// - For dot notation (e.g. `foo.bar`), TypeScript will always error that the unknown key does not | |
// exist. | |
// - For bracket notation (e.g. `foo['bar']`), TypeScript will fallback to using the index signature | |
// if there is one. If the type doesn't have an index signature, the type will be inferred as | |
// `any`. This means these errors will only be visible when the `noImplicitAny` compiler option is | |
// enabled, however it is possible to write a function to force TypeScript to only check the | |
// property types and not the index signature. | |
// | |
{ | |
// Inferred type | |
const record = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string = record.a | |
const b: string = record['b'] | |
// Error: Property 'x' does not exist on type '{ a: string; b: string; }'. | |
const x: string = record.x | |
// Error: Element implicitly has an 'any' type because type '{ a: string; b: string; }' has no | |
// index signature. | |
const z: string = record['z'] | |
} | |
{ | |
// Type annotation | |
const record: { a: string; b: string; } = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string = record.a | |
const b: string = record['b'] | |
// Error: Property 'x' does not exist on type '{ a: string; b: string; }'. | |
const x: string = record.x | |
// Error: Element implicitly has an 'any' type because type '{ a: string; b: string; }' has no | |
// index signature. | |
const z: string = record['z'] | |
} | |
{ | |
// Mapped type type annotation | |
type UserId = 'a' | 'b'; | |
const record: { [key in UserId]: string } = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string = record.a | |
const b: string = record['b'] | |
// Error: Property 'x' does not exist on type '{ a: string; b: string; }'. | |
const x: string = record.x | |
// Error: Element implicitly has an 'any' type because type '{ b: string; a: string; }' has no | |
// index signature. | |
const z: string = record['z'] | |
} | |
{ | |
// Record helper type annotation | |
type UserId = 'a' | 'b'; | |
const record: Record<UserId, string> = { | |
a: 'foo', | |
b: 'bar', | |
} | |
const a: string = record.a | |
const b: string = record['b'] | |
// Error: Property 'x' does not exist on type 'Record<UserId, string>'. | |
const x: string = record.x | |
// Error: Element implicitly has an 'any' type because type 'Record<UserId, string>' has no | |
// index signature. | |
const z: string = record['z'] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment