Cuel
Cuel

Reputation: 2630

Typescript definition for a function which returns based on nested keys in parameter

I'm trying to write a declaration file for a library we're using without any means to modify it.

The way it works is that you can send a configuration object, the return will be based on some keys inside of it

const args = {
  // ...
  resources: {
    One: {
      query: { url: 'value' }
    }
  } 
}

library(args)

Invoking it returns an object with the deeply nested keys of resources as functions

const ret = {
  One: {
    query: async (query) => <data>
 }
}

// ret.One.query(args) => data

Ideally each <data> would also be typed, I'm not sure if this is possible due to the dynamic keys? I've tried a couple of approaches using keyof the parameters without any luck

Edit: Updated example

const config = {
  // ...
  resources: {
    User: {
      find: { url: 'http://example.com/userapi' }
    },
    Dashboard: {
      update: { method: 'post', url: 'http://example.com/dashboardapi'}
    }
  } 
}

const services = serviceCreator(config)

// service creator turns the supplied resources object into promise functions

await services.User.find({id: '123'}) // Promise<User>
await services.Dashboard.update({user: '123', action: 'save'}) // Promise<Dashboard>

Upvotes: 1

Views: 70

Answers (2)

Derek Nguyen
Derek Nguyen

Reputation: 11577

I'm fairly new to TS & find this question really interesting. Here's my take on it, it's fairly verbose & require a weird type input. However it gets pretty good type suggestion. I would love feedback from more experienced users:

/**
 * map dynamic resource type to method name,
 * i.e 'User' -> 'find' | 'lookup'
 */
type MethodMap = Record<string, string>

interface ResourceParams {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
}

/**
 * Each method in resource type has interface ResourceParams
 */
type Resources<T extends MethodMap> = {
  [K in keyof T]: {
    [S in T[K]]: ResourceParams;
  }
}


interface Config<T extends MethodMap> {
  resources: Resources<T>;
  [key: string]: unknown;
}

type Result<T extends MethodMap> = {
  [K in keyof T]: {
    [S in T[K]]: () => Promise<any>
  }
}

declare const library: <T extends MethodMap>(config: Config<T>) => Result<T>

// usage

const result = library<{ User: 'find' | 'lookup'; Dashboard: 'search' }>({
  resources: {
    User: {
      find: { url: 'value' },
      lookup: { url: 'asdads', method: 'GET' },
    },
    Dashboard: {
      search: { url: 'value' }
    },
  } 
})

result.User.find() // OK
result.User.lookup() // OK
result.Dashboard.search() // OK

result.User.search() // Not OK
result.Dashboard.find() // Not OK
result.Store.find() // Not OK

Link to playground

Upvotes: 1

Mu-Tsun Tsai
Mu-Tsun Tsai

Reputation: 2534

Without further information, I'm guessing that you're looking for something like this:

type Arg<T> = { resources: T; }
type Ret<T> = {
    [K in keyof T]: {
        query: (...query: any[]) => Promise<any>;
    }
}
declare const library: <T>(arg: Arg<T>) => Ret<T>;

Let's test it to see if it works:

const args = {
    resources: {
        One: {
            query: { url: 'value' }
        }
    }
}
var ret = library(args);

async function test() {
    await ret.One.query();  // OK
}

You can try it in this playground.

Upvotes: 2

Related Questions