Home > Enterprise >  Can I create a TypeScript type where certain properties are an ID/array of IDs that can be expanded
Can I create a TypeScript type where certain properties are an ID/array of IDs that can be expanded

Time:12-01

For context, in my database I have one-to-one and one-to-many relationships, which I'm trying to express in typescript. Instead of creating several variations of a type, I would like to create one type that allows me to expand relationships using utility types I'm creating (e.g. ExpandOne<>, ExpandAll<>).

Simplified working example

Spec

Everything I would like to have

Is this even possible?

// UTILITIES

type ID = string

type Expandable<Entity> = ID | Entity

type ExpandableArray<Entity> = undefined | Entity[]

type Car<L extends any[]> = L extends [infer Head, ...infer _] ? Head : never

type Cdr<L extends any[]> = L extends [infer _, ...infer Rest] ? Rest : never

type ExpandOne<T extends Record<string, any>, Expand extends any[] | []> = {
  [Key in keyof T]: T[Key] extends Expandable<infer Entity extends Record<string, any>>
    ? Key extends Car<Expand>
      ? ExpandOne<Entity, Cdr<Expand>>
      : string
    : T[Key]
}

// TODO: Call ExpandOne on multiple properties
// type ExpandAll

// DATABASE TYPES

interface PostCategory {
  description: string
}

interface PostComment {
  content: string
}

interface Post {
  content: string
  comments: ExpandableArray<PostComment>
  category: Expandable<PostCategory>
}

interface User {
  username: string
  posts: Expandable<Post[]>
  comments: ExpandableArray<PostComment>
}

// USAGE
// expand one level deep
const expandedUserPost: ExpandOne<User, ['posts']> = {} as any
expandedUserPost.posts

// expand two levels deep
const expandedUserPostComments: ExpandOne<User, ['posts', 'comments']> = {} as any
expandedUserPostComments.posts[0].comments

// expand multiple
const expandMultiple: ExpandAll<User, [['posts', 'comments'], ['comments']]> = {} as any
expandMultiple.posts[0].comments
expandMultiple.comments

// bonus!
// would be nice if ExpandAll<> assumed that any Expandable<> that hasn't been expanded is a string (database id) and any ExpandableArray<> that hasn't been expanded is undefined
const expandNonePost: ExpandAll<Post, []> = {} as any
expandNonePost.category // type should be string

// NOTE
// Expandable<> is similar to ExpandableArray<> 
// except when something is not exapnded instead of being undefined its a database id (string)
const expandPost: ExpandOne<Post, ['category']> = {} as any
expandPost.category.description

What I want to avoid

interface Posts {
  content: string
}

interface User {
  username: string
  posts: undefined
}

// I would prefer not to create multiple variations of each type
interface UserExpandedPosts {
  username: string
  posts: Posts[]
}

The big picture

// Using this system I can create a JavaScript function that tells 
// my backend to expand relationships and my types at the same time

function fetchData(configFunction, expansions) {
  return fetch()
}

const data = await fetchData(getUser(), [['posts'], ['somethingElse']])

// Assuming the function getUser() has an expandable database type 
// associated with it, fetchData would know how to both ask my 
// backend to expand the entity as well as expanding matching TypeScript type 

// In other words as long as my expansion doesn't fail, the return type of
// fetchData would always match the data (including expansions I asked for)

CodePudding user response:

// UTILITIES
declare const explandBrand: unique symbol

type Expandable<Entity> = string & { [explandBrand]: Entity }
//   

type Expand1<T, E extends string[]> =
  | T extends Expandable<infer X extends any[]> ? Expand1<X[number], E>[]
  : T extends Expandable<infer X> ? Expand1<X, E>
  : E extends [infer F extends keyof T, ...infer L extends string[]] ?
  | {
    [K in keyof T]: F extends K ? Expand1<T[K], L> : T[K]
  }
  : T;

type x1 = Expand1<User, ['posts']>
//   ^?

type x3 = Expand1<x1, ['comments']>
//   ^?

type x4 = Expand1<User, ['posts', 'comments']>
//   ^?


type Expand2<T, A extends string[][]> =
| A extends [infer F extends string[], ...infer L extends string[][]] ? Expand2<Expand1<T, F>, L>
: T

type x5 = Expand2<User, [['posts', 'comments'], ['comments']]>
//   ^?

Playground

It is string for ids, for arrays it's also a string id, modify if you need that

  • Related