[Typescript] The type Registry pattern (declare module)
Our project might have a file structure like
Our project might have a file structure like
data/
book.ts // A model for Book records
magazine.ts // A model for Magazine records
lib/
registry.ts // Our type registry, and a `fetchRecord` function
index.ts // Entry point
Let’s focus on that first argument of the fetchRecord
function. We can create a “registry” interface that any consumer of this library can use to “install” their resource types, and define the fetchRecord
function using our new keyof
type query.
// @filename: lib/registry.ts
export interface DataTypeRegistry
{
// empty by design
}
// the "& string" is just a trick to get
// a nicer tooltip to show you in the next step
export function fetchRecord(arg: keyof DataTypeRegistry & string, id: string) {
}
Now let’s focus our attention toward “app code”. We’ll define classes for Book
and Magazine
and “register” them with the DataTypeRegistry
interface
// @filename: data/book.ts
export class Book {
deweyDecimalNumber(): number {
return 42
}
}
declare module "../lib/registry" {
export interface DataTypeRegistry {
book: Book
}
}
// @filename: data/magazine.ts
export class Magazine {
issueNumber(): number {
return 42
}
}
declare module "../lib/registry" {
export interface DataTypeRegistry {
magazine: Magazine
}
}
Now look what happens to the first argument of that fetchRecord
function! it’s "book" | "magazine"
despite the library having absolutely nothing in its code that refers to these concepts by name!
// @filename: index.ts
import { DataTypeRegistry, fetchRecord } from './lib/registry'
fetchRecord("book", "bk_123")
// (alias) fetchRecord(arg: "book" | "magazine", id: string): void
Let's make one step further, so that when calling fetchRecord
function, it restrict id
, if you are getting book
, id should start with book_
;
And the function return type should return Book
instead of void
export interface DataTypeRegistry {
// empty by design
}
// the "& string" is just a trick to get
// a nicer tooltip to show you in the next step
export function fetchRecord<
K extends keyof DataTypeRegistry & string,
P extends `${K}_${string}`,
>(arg: K, id: P): DataTypeRegistry[K] {
return {} as any
}
export function fetchRecords<
K extends keyof DataTypeRegistry & string,
P extends `${K}_${string}`,
>(arg: K, ids: P[]): DataTypeRegistry[K][] {
return {} as any
}
Test code:
import { fetchRecord, fetchRecords } from './lib/registry'
async function main() {
const myBook = await fetchRecord('book', 'book_123')
const myMagazine = await fetchRecord('magazine', 'magazine_123')
const myBookList = await fetchRecords('book', ['book_123'])
const myMagazineList = await fetchRecords('magazine', [
'magazine_123',
])
//@ts-expect-error
fetchRecord('book', 'booooook_123')
//@ts-expect-error
fetchRecord('book', 'magazine_123')
//@ts-expect-error
fetchRecord('magazine', 'mag_123')
//@ts-expect-error
fetchRecord('magazine', 'book_123')
//@ts-expect-error
fetchRecords('book', ['booooook_123'])
//@ts-expect-error
fetchRecords('magazine', ['mag_123'])
}