Skip to content

Instantly share code, notes, and snippets.

@FlandreDaisuki
Forked from OliverJAsh/foo.ts
Created March 30, 2021 02:07
Show Gist options
  • Save FlandreDaisuki/99ca925e5ea986141ae932d29940714b to your computer and use it in GitHub Desktop.
Save FlandreDaisuki/99ca925e5ea986141ae932d29940714b to your computer and use it in GitHub Desktop.
Records and dictionaries in TypeScript
/*
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