#Typesafety while building an object

6 messages · Page 1 of 1 (latest)

trail atlas
#

Hi everyone! I need some help with TypeScript to achieve type safety for an object while building it. I have a function foo that receives an object with properties. Let's say it has two properties, foo and baz. Each property will have the same config object. The config object has the following shape:

type ConfigObject = {
  action: (...args: any[]) => Promise<any>;
  dependencies: string[];
}

The types are quite broad, but this is just for demonstration purposes. I want the type system to infer the arguments of my action function when a config object specifies a dependency. Instead of any, it should infer an object with properties equal to my dependency array (which will have type safety for only introducing properties of my initial root object), and with the values equal to the return type of each of those dependencies, respectively.

For example:

const obj = {
  foo: {
    action: () => {
      return [1, 2, 3];
    },
  },
  baz: {
    action: ({ foo }) => {
      const fooResult = foo; // should be inferred as [1, 2, 3]
      fooResult.forEach((value) => console.log(value)); // 1, 2, and 3
    },
  },
};

Please note that the dependencies array could be optional, and we want to maintain type safety for it as well. How can I achieve this level of type safety and inference for my object?
Any help would be greatly appreciated!

#

This is my hardest honest unsuccessful attempt to achieve it, but I end up with my arguments as any

type DependencyMap<T extends Record<string, ConfigObject<any, any>>> = {
  [K in keyof T]: ReturnType<T[K]['action']> extends Promise<infer R> ? R : never;
};

type ResolveDependencies<
  T extends Record<string, ConfigObject<any, any>>,
  D extends readonly (keyof T)[]
> = {
  [K in D[number]]: DependencyMap<T>[K];
};

type ConfigObject<T extends Record<string, ConfigObject<any, any>>, D extends readonly (keyof T)[] = [], R = any> = {
  action: (deps: ResolveDependencies<T, D>) => Promise<R>;
  dependencies?: D;
};

type ConfiguredObject<T extends Record<string, ConfigObject<any, any>>> = T;

function createObject<const T extends ConfiguredObject<any>>(arg: T): T {
  return arg;
}

const obj = createObject({
  foo: {
    action: () => {
      return Promise.resolve([1, 2, 3]);
    },
  },
  baz: {
    action: ({ foo }) => {
      const fooResult = foo; // should be infered as [1,2,3] or at least number[]
      fooResult.forEach((value) => console.log(value)); // 1, 2, 3
      return Promise.resolve();
    },
    dependencies: ['foo'],
  },
});

noble bolt
#

Hey, thanks for the thorough description 😄
I think you are on the right track, I think it will work with a few adjustments

#

would you like me to just fix the code or would you like some tips on how to do it yourself?

trail atlas
#

up to this point I'm phishing for solutions :/
I actually came up with a workaround using a builder pattern and building the object step by step.
This way I can maintain the whole config object on the builder and pass it around with exquisite type safety 🫶
But, for educational purposes, please let me know how would you achieve this for a single object, it would be much appreciated!

trail atlas
#

@noble bolt fyi ☝️