koi
koi

Reputation: 26

Typescript Map of generics

Im trying to get each Field in ObjectType to have its own generic, so the resolve fn of each Field has to return the type defined in the type property.

type Scalars = {
  ID: string;
  Int: number;
  Float: number;
  String: string;
  Boolean: boolean;
};

type Field<T extends keyof Scalars> = {
  type: T,
  resolve: () => Scalars[T];
}

type ObjectType<T extends keyof Scalars> = {
  [key: string]: Field<T>
}

const objectType = <T extends keyof Scalars>(config: ObjectType<T>): any => 'todo';

objectType({
  id: {
    type: 'Int',
    // type of `resolve` is `() => (string | number)`, should be `() => number`
    resolve: () => '123',
  },
  name: {
    type: 'String',
    // should be `() => string`
    resolve: () => 123,
  },
});

After investigating some other SO questions I found a solution that almost solves my problem in https://stackoverflow.com/a/51547767/11042710

Here's the TS playground where I attempt what was suggested above TS playground.

But it fails when I try to pass the infer P type to a generic that extends keyof Scalars

Upvotes: 0

Views: 722

Answers (1)

jcalz
jcalz

Reputation: 327654

It looks like your Field<..> types can form a discriminated union like this:

type Fields = { [K in keyof Scalars]: Field<K> }[keyof Scalars];

Here the type Fields is equivalent to

type Fields = Field<"ID"> | Field<"Int"> | Field<"Float"> | 
  Field<"String"> | Field<"Boolean">

where all terms in the union have a common type filed of a string literal type that can be used to discriminate between them. That's good, because it means that your objectType() method can involve a lot less generic hoop-jumping, since the config property values can be constrained to the specific type Fields:

const objectType = <T extends Record<keyof T, Fields>>(config: T): any => 'todo';

Now all we're doing is constraining config to be an object with any keys (Record<keyof T, ...>) and whose values are all Fields. Let's see if it works. This is is the intended use:

objectType({
  id: {
    type: 'String',
    resolve: () => "123",
  },
  ide: {
    type: 'Int',
    resolve: () => 123,
  },
});

and this is what happens when you do it wrong:

objectType({
  id: { // error! number is not assignable to string
    type: 'String',
    resolve: () => 123,
  },
  ide: { // error! string is not assignable to boolean
    type: 'Boolean',
    resolve: () => "123",
  },
});

Those are the errors you expect, right?

Playground link to code

Upvotes: 2

Related Questions