Search code examples
typescripttypescript-typingsmapped-types

Dynamically extract type in TypeScript


I am trying to dynamically map one type to another based on certain flags, which might result in optional fields.

type Constraint = {
  required: boolean,
  callback: () => any,
}

type Schema = Record<string, Constraint>;

const mySchema: Schema = {
  bar: {
    required: true,
    callback: () => 1
  },
  foo: {
    required: false,
    callback: () => true
  }
}

type MapSchemaToOutput<T extends Schema> = {
  [K in keyof T as T[K]['required'] extends true ? K : never]: ReturnType<T[K]['callback']>
} & {
  [K in keyof T as T[K]['required'] extends false ? K : never]?: ReturnType<T[K]['callback']>
}

type Output = MapSchemaToOutput<typeof mySchema>;

The end goal is to have Output equal:

{
  bar: number,
  foo?: boolean
}

I know I can do the mapping by hand, interested to know if this can be done dynamically.


Solution

  • You approach works as-is, with one change.

    The issue is that the : Schema annotation is "throwing away type information":

    const mySchema: Schema = {
       //...
    };
    

    With that annotation, TS only remembers that mySchema is Record<string, Constraint>, not any of the specific structure of the object.


    One fix is as const:

    const mySchema = {
        //...
    } as const;
    

    This preserves the literal types within the object. However, there's no longer any constraints on the contents of mySchema, and any errors defining mySchema would have to be caught by the usage, rather than at definition-time.


    A better fix is to use a helper function to introduce a constraint, without annotating the type directly:

    function buildSchema<T extends Schema>(schema: T) { return schema; }
    
    const mySchema = buildSchema({
       //...
    });
    

    Due to the <T extends Schema> constraint, TS will raise an error, as before, if the schema object doesn't match the specified type.

    But unlike annotating the object's type, this type returned by this function is unchanged from the literal object which is passed to the function: so no type information is lost.

    With this change, the rest of the types work as expected