๐ŸŽ“ A1Learner
๐Ÿ”
0 of 42 lessons completed

TypeScript Project: Typed Data Layer

A typed data layer stores records of one shape and answers lookups with typed results. This capstone builds a small generic repository with a patch update, a Result for missing records, and a readonly view.

A generic repository over an array

The class stores one record shape and finds records by id. The constraint promises the id field the find method needs.

interface HasId {
  id: number;
}

class Repository<T extends HasId> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  find(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }
}

interface Book {
  id: number;
  title: string;
}

const repo = new Repository<Book>();
repo.add({ id: 1, title: 'Dune' });
repo.add({ id: 2, title: 'Emma' });

const found = repo.find(2);
if (found) {
  console.log(found.title);
}

Patch updates with a Result

update takes a Partial patch and returns a Result. A missing id returns the failure shape instead of throwing.

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

class Repository<T extends { id: number }> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  update(id: number, patch: Partial<T>): Result<T, string> {
    const item = this.items.find((entry) => entry.id === id);
    if (!item) {
      return { ok: false, error: 'No record ' + id };
    }
    Object.assign(item, patch);
    return { ok: true, value: item };
  }
}

interface Book {
  id: number;
  title: string;
  pages: number;
}

const repo = new Repository<Book>();
repo.add({ id: 1, title: 'Dune', pages: 412 });

const updated = repo.update(1, { pages: 688 });
if (updated.ok) {
  console.log(updated.value.pages);
}

const missing = repo.update(9, { pages: 10 });
if (!missing.ok) {
  console.log(missing.error);
}

A readonly view of all records

all returns the records as a ReadonlyArray. The caller reads the view freely, and the layer keeps ownership of the store.

class Repository<T extends { id: number }> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  all(): ReadonlyArray<T> {
    return this.items;
  }
}

interface Book {
  id: number;
  title: string;
}

const repo = new Repository<Book>();
repo.add({ id: 1, title: 'Dune' });
repo.add({ id: 2, title: 'Emma' });

const view = repo.all();
console.log(view.length);
for (const book of view) {
  console.log(book.title);
}

Example

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

class Repository<T extends { id: number }> {
  private store = new Map<number, T>();

  add(item: T): void {
    this.store.set(item.id, item);
  }

  find(id: number): T | undefined {
    return this.store.get(id);
  }

  update(id: number, patch: Partial<T>): Result<T, string> {
    const item = this.store.get(id);
    if (!item) {
      return { ok: false, error: 'No record ' + id };
    }
    Object.assign(item, patch);
    return { ok: true, value: item };
  }

  all(): ReadonlyArray<T> {
    return [...this.store.values()];
  }
}

interface Book {
  id: number;
  title: string;
  pages: number;
}

const repo = new Repository<Book>();
repo.add({ id: 1, title: 'Dune', pages: 412 });
repo.add({ id: 2, title: 'Emma', pages: 320 });

const found = repo.find(1);
if (found) {
  console.log(found.title);
}

const updated = repo.update(2, { pages: 336 });
if (updated.ok) {
  console.log(updated.value.pages);
}

const missing = repo.update(9, { pages: 10 });
if (!missing.ok) {
  console.log(missing.error);
}

console.log(repo.all().length);

Key points

  • The Repository class is generic over T extends { id: number }, so every stored record has a numeric id.
  • The find method returns T or undefined, because a lookup can miss.
  • The update method takes a Partial<T> patch and returns a Result that names the missing-record case.
  • The all method returns a ReadonlyArray view, so callers can read every record and change none of them through the view.
  • One Map holds the records in the closing example, and its values keep insertion order.

In this example, the repository adds two books, finds one by id, patches the pages of the other, reports a missing record through a Result, and counts the readonly view.

Note

Every piece of this lesson appeared earlier in the course, and the capstone only combines them.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

What does update return when the id is missing?