Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use an object type definition to construct a new array/tuple type?

If I have an existing type definition of:

type Person = {
  name: string;
  age: number;
  sayHello: (greeting: string) => void;
}

is it possible to some how build a typed array/tuple of the keys AND types of this definition?

For instance if I would like to have a type like:

type PropAndTypes = ( 
  ['name',string] | 
  ['age',number]  | 
  ['sayHello', (arg: string) => void]
)[];

Can this somehow be generated from the existing type definition?

For less of an esoteric example, the specific use case would be to help create type safe State Declarations for a routing library (ui-router) given the expected props of the linked component.

An example state declaration has a component field, and the resolves that will be passed to the component.

const exampleStateDefinition = {
  name: 'example.state',
  url: '/:name/:age',
  component: ExampleComponent,
  resolve: [
    {
      token: 'age' as const,
      deps: [Transition],
      resolveFn: (trans: Transition) => trans.params<Person>().age,
    },
    {
      token: 'name' as const,
      deps: [Transition],
      resolveFn: (trans: Transition) => trans.params<Person>().name,
    },
    {
      token: 'sayHello' as const,
      deps: [Transition],
      resolveFn: () => (greeting: str) => console.log(greeting),
    },
  ],
};

I would like to generate a type definition like:

type stateDeclaration = {
    resolve: ResolveTypes[];
}

where ResolveTypes is

type ResolveTypes = { token: 'name', resolve: string } | { token: 'age', resolve: number }; 
like image 982
John Lenehan Avatar asked Dec 22 '25 18:12

John Lenehan


1 Answers

So here's what I came up with for the "less of an esoteric example" (playground):

const ExampleComponent = (props: { hey: string }) => ({ hey: props.hey });
interface Person {
  age: number;
  name: string;
}
interface Transition {
  params: <T>() => T;
}
const transition = { params: () => ({ age: 1, name: "aaa" }) };

const exampleStateDefinition = {
  name: "example.state",
  url: "/:name/:age",
  component: ExampleComponent,
  resolve: [
    {
      token: "age" as const,
      deps: [transition],
      resolveFn: (trans: Transition) => trans.params<Person>().age,
    },
    {
      token: "name" as const,
      deps: [transition],
      resolveFn: (trans: Transition) => trans.params<Person>().name,
    },
    {
      token: "sayHello" as const,
      deps: [transition],
      resolveFn: () => (greeting: string) => console.log(greeting),
    },
  ],
};

type ResolverIn<T, R> = {
  token: T;
  resolveFn: R;
};
type ResolverOut<T, R> = {
  token: T;
  resolve: R;
};

type T1<T> = T extends Array<infer U>
  ? U extends ResolverIn<infer V, infer R>
    ? R extends (...args: any) => infer Ret
      ? ResolverOut<V, Ret>
      : never
    : never
  : never;

type ResolveTypes = T1<typeof exampleStateDefinition["resolve"]>;
// = ResolverOut<"age", number> | ResolverOut<"name", string> | ResolverOut<"sayHello", (greeting: string) => void>
// which is:
// { token: "age", resolve: number } | { token: "name", resolve: string } | { token: "sayHello", resolve: (greeting: string) => void }

type StateDeclaration = {
  resolve: ResolveTypes;
};

With the meat of the matter being

type T1<T> = T extends Array<infer U>
  ? U extends ResolverIn<infer V, infer R>
    ? R extends (...args: any) => infer Ret
      ? ResolverOut<V, Ret>
      : never
    : never
  : never;

Here I first use a conditional to ensure the type is an array and infer the type of the array elements, then infer the shape to be ResolverIn (sort of a Pick<>), then infer the return type of the resolveFn function (like ReturnType<T>, but we've just inferred the type so we need to infer again to further constrain the type to be a function) and finally produce the shape that we want which is ResolverOut<V, Ret>.

The type of ResolveTypes thus becomes:

ResolverOut<"age", number> |
ResolverOut<"name", string> |
ResolverOut<"sayHello", (greeting: string) => void>

whose shape is equivalent to:

{ token: "age"; resolve: number } |
{ token: "name"; resolve: string } |
{ token: "sayHello"; resolve: (greeting: string) => void }

Additionally, your example excludes the resolver types whose return value is a function, which can be filtered out with another conditional:

type T1<T> = T extends Array<infer U>
  ? U extends ResolverIn<infer V, infer R>
    ? R extends (...args: any) => infer Ret
      ? Ret extends (...args: any) => any
        ? never
        : ResolverOut<V, Ret>
      : never
    : never
  : never;

Edit: Now, I didn't get the chance to test this, but to produce StateDeclaration directly from typeof exampleStateDefinition, you can probably do something like this:

type T2<T> = T extends { resolve: infer U } ? { resolve: T1<U> } : never;
type StateDeclaration = T2<typeof exampleStateDefinition>;

Edit 2: I was able to get a bit closer to what you clarified in the comments with this answer which uses an utility function (which just returns the array passed to it as-is) to enforce that the array passed to it contains all elements from the union type. Playground.

interface Person {
  age: number;
  name: string;
}
interface Transition {
  params: <T>() => T;
}

type ResolveType<T> = {
  [K in keyof T]: { token: K; resolveFn: (...args: any[]) => T[K] };
}[keyof T];

type ResolveTypes<T> = ResolveType<T>[]


function arrayOfAll<T>() {
  return function <U extends T[]>(array: U & ([T] extends [U[number]] ? unknown : 'Invalid')) {
    return array;
  };
}

interface CustomStateDeclaration<T> {
  name: string;
  url: string;
  component: any;
  resolve: ResolveTypes<T>;
}

type ExampleComponentProps = {
  age: number;
  name: string;
  sayHello: (greeting: string) => string;
};

const arrayOfAllPersonResolveTypes = arrayOfAll<ResolveType<ExampleComponentProps>>()
// passes
const valid = arrayOfAllPersonResolveTypes([
  {
    token: "age" as const,
    resolveFn: (trans: Transition) => trans.params<Person>().age,
  },
  {
    token: "name" as const,
    resolveFn: (trans: Transition) => trans.params<Person>().name,
  },
  {
    token: "sayHello" as const,
    resolveFn: () => (greeting: string) => `Hello, ${greeting}`,
  },
])

// error; missing the "sayHello" token
const missing1 = arrayOfAllPersonResolveTypes([
  {
    token: "age" as const,
    resolveFn: (trans: Transition) => trans.params<Person>().age,
  },
  {
    token: "name" as const,
    resolveFn: (trans: Transition) => trans.params<Person>().name,
  }
])

// error; "name" token's resolveFn returns a number instead of a string
const wrongType = arrayOfAllPersonResolveTypes([
  {
    token: "age" as const,
    resolveFn: (trans: Transition) => trans.params<Person>().age,
  },
  {
    token: "name" as const,
    resolveFn: (trans: Transition) => 123,
  },
  {
    token: "sayHello" as const,
    resolveFn: () => (greeting: string) => `Hello, ${greeting}`,
  },
])

Could try making a type that does what the utility function does, or create a state definition factory/constructor that all state declarations need to be created with (enforced by a symbol, perhaps), which uses that utility function.

like image 102
cbr Avatar answered Dec 24 '25 07:12

cbr



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!