2

I'm trying to get TS generics to map to a new object. In short, I'm trying to convert:

{ 
  key: { handler: () => string },
  key2: { hander: () => number },
}

to:

{ key: string, key2: number }

full example:

type programOption = {
  validator: () => unknown
}

type programConfig<T extends {[key: string]: programOption} = {}> = {
  options: T,
  handler: (data: mapProgramConfig<T>) => void,
}

type mapProgramConfig<T extends {[key: string]: programOption}> = {
  [K in keyof T]: ReturnType<programOption['validator']>
}

type mapProgramConfigHardcoded<T> = {
  fruit: string,
  animal: number
}

class Program {
  constructor (config: programConfig) {}
}

const foo = new Program({
  options: {
    'fruit': { validator: () => 'asdf' },
    'animal': { validator: () => 42 },
  },
  handler: ({fruit, animal, thing}) => {

  },
});

Exactly what I'm trying to do can be seen if you replace mapProgramConfig with mapProgramConfigHardcoded in the programConfig type, but I can't seem to make it work in the generic case.

ts playground link

3 Answers 3

3

Consider this solution:

type ProgramOption<T> = {
  validator?: () => T
}


type Convert<Obj extends Record<string, ProgramOption<any>>> = {
  [Prop in keyof Obj]: Obj[Prop]['validator'] extends () => infer Return ? Return : never
}
const program = <
  Keys extends PropertyKey,
  ValidatorValues extends string | number,
  Options extends Record<Keys, ProgramOption<ValidatorValues>>,
  Handler extends (data: Convert<Options>) => void,
  >(data: { options: Options, handler: Handler },) => {
  return data
}

const foo = program({
  options: {
    'fruit': { validator: () => 'string' },
    'animal': { validator: () => 42 },
  },
  handler: (obj) => {
    obj.animal // 42
    obj.fruit // 'string'
  }
});

Playground

In order to infer obj argument in handler property you need infer all nested keys and values.

Keys - refers to fruit and animal keys of nested object ValidatorValues - refers to Validator return type Options - refers to whole options property Handler - refers to handler accordingly.

I have used Convert utility type to iterate through Options type and grab all return types of validator property

If you are interested in function arguments inference you can check my article

I have used program function instead of Program class because Type parameters cannot appear on a constructor declaration

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! I think this provides what I need to make it work. Simplifying it and converting it back to the format I need now!
Thanks! Works great, I removed the requirement to manually define the return types (string | number) and the extra generics seem to be unnecessary: tsplay.dev/NBe7nw, but this is exactly what I needed :)
@Nobody you are welcome. I always trying to add more generic than it is necessary :D
1

You can define it like this

type programConfig<T extends Record<string, any>> = {
  options: {
    [K in keyof T]: {
      validator: () => T[K]
    }
  },
  handler: (data: T) => void, // ??? should be {optionKey: inferred return type}
}

class Program<T extends Record<string, any> = Record<string, any>> {
  constructor(config: programConfig<T>) { }
}

and when you call it the generic will be inferred only with the necessary data (the key and the data type)

const foo: Program<{
    fruit: string;
    animal: number;
}>

TS Playground

1 Comment

nice solution! a bit simplier than mine
0

you just need to extend the generic defined on the programConfig interface to the Program class:

class Program<T extends {[key: string]: programOption} = {}> {
  constructor (config: programConfig<T>) {}
}

1 Comment

Almost, but now fruit and animal are both unknown, as defined in validator: () => unknown

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.