2

How do I nest the same type that uses generics?

interface programConfig<T extends Record<string, any>> {
  // other types removed; not relevant to the question
  commands?: { [key: string]: programConfig<???> }; // how do I type this?
}

More complete ts playground example that shows what I'm trying to accomplish

3
  • What do you want that type to be? Pass in T if you want it to be the exact same. Commented Nov 7, 2021 at 14:14
  • @Chase - I want it to be its own record. The record is currently inferred based on the calling code. If I pass in T then all nested instances use the same record as the root instance, which isn't what I want. The example should be more clear Commented Nov 7, 2021 at 14:17
  • I have the feeling it's not possible. You would need commands to be generic, but in TypeScript, only type aliases, interfaces, classes, constructors, functions, and methods can be generic, but properties, objects, and values can't. You would essentially need commands?<U>: { [key: string]: programConfig<U> }; See github.com/microsoft/TypeScript/issues/17574 Commented Nov 7, 2021 at 14:42

2 Answers 2

1

You can specify a second generic to encompass the sub elements of the programConfig, in this example I constrained the inner ones to not allow a 3rd level of nesting since supporting arbitrary nesting would be annoying and hopefully not necessary

playground


interface BaseProgramConfig<T extends Record<string, unknown> >{
  options?: {
    [K in keyof T]: {
      validator?: () => T[K]
    }
  },
  handler?: (data: T) => void
}
interface programConfigWithCommands<T extends Record<string, unknown>, Sub extends Record<string, Record<string, unknown>>> extends BaseProgramConfig<T> {
  commands?: {[K in keyof Sub]: BaseProgramConfig<Sub[K]>}
}

class Program<T extends Record<string, unknown>, Comms extends Record<string, Record<string, unknown>>> {
  constructor(config: programConfigWithCommands<T,Comms>) { }
}

const foo = new Program({
  options: {
    'fruit': { validator: () => 'asdf' },
    'animal': { validator: Number },
  },
  handler: ({ fruit, animal, thing }) => { // fruit and animal are properly typed based on options above
    console.log(fruit, animal)
  },
  commands: {
    foo: {
      options: {
        'tree': { validator: () => 'asdf' },
        'person': {},
      },
      handler: ({ tree, person, thing }) => { // tree is typed as string, person is typed as unknown
        console.log(tree, person)
      },
    }
  }
});

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

3 Comments

I was playing with something like this but couldn't get it to work. This doesn't either, as the args in the last handler are still typed as any. Also it unfortunately does need arbitrary levels of nesting (or at least one more practically speaking). This is legacy code that I was trying to not change (just add types to), but I'm leaning towards ditching the nested config in favor of {commands: new Program({...})} which would be a fairly minimal codemod.
@Nobody I updated it so it works properly with one level of nesting, I'm not sure how you could do it with 3 levels just because I'm not sure where those sub-elements would even be stored.
@TadhgMcDonald-Jensen thank you for editing. I have tried F-bounded quantification but it does not work
1

You just need to call new Program again, like here:

type programConfig<T extends Record<string, any> = Record<string, any>> = {
  options?: {
    [K in keyof T]: {
      validator?: () => T[K]
    }
  },
  handler?: (data: T) => void,
  commands?: { [key: string]: Program<Record<string,unknown>> }; // here use sub-programs that have nothing to do with T
}

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

const foo = new Program({
  options: {
    'fruit': { validator: () => 'asdf' },
    'animal': { validator: Number },
  },
  handler: ({ fruit, animal, thing }) => { // fruit and animal are properly typed based on options above
    console.log(fruit, animal)
  },
  commands: {
    foo: new Program({
      options: {
        'tree': { validator: () => 'asdf' },
        'person': {},
      },
      handler: ({ tree, person, thing }) => { // tree and person are typed in the same way and Program of any type is accepted in commands
        console.log(tree, person)
      },
    })
  }
});

Playground

1 Comment

Yeah, this is my fallback plan :) But it's legacy code that's used all over our codebase so I'm trying to avoid it

Your Answer

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