[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 Bookinstead 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'])
}

 

posted @ 2024-01-31 16:02  Zhentiw  阅读(14)  评论(0编辑  收藏  举报