1

I'm working in a project that uses a generator to pull in interfaces from a GraphQL schema. We use a pattern in the project where we create our Component interfaces from the GraphQL schema interfaces that are generated for queries that the components make for data (so the props power the queries that power the components). From these GraphQL interfaces, I often need to customize my component interface since some fields may be required, others optional, and others removed because they're hardcoded.

For the props on a current component, I have a situation I haven't run into before. In this case, only one of the fields in the GraphQL schema interface is required, another is hardcoded internally (so should be excluded from my component props), and the rest are all optional. I'll simplify the classes for the sake of not adding any complexity to an already seemingly complex usecase.

Generated GraphQL Interface type (the base type to create the Component interface from):

MyBaseInterface: {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
  prop5: string;
  prop6: string;
}

I want to create an interface type using the base generated interface. The desired component interface should look like this:

MyComponentInterface: {
  prop1!: string;
  prop2!: string;
  prop4?: string;
  prop5?: string;
  prop6?: string;
}

Notice how prop3 is removed, prop1 & prop2 are required, and the rest of the props are optional? Most of the values get defaulted in the component for backward compatibility with earlier implementations. The new Component interface should always be based on the MyBaseInterface, and the Component interface is expected to change over time.

My team typically uses the typing utils from TypeScript library for creating component types, but this usecase goes a ways beyond the basic scope of what TS provides in their utils. We have usually just needed stuff like:

export type MyNewInterface = Pick<MyBaseInterface, 'prop1' | 'prop2' | 'prop4'>;

But my current case is more complex than just picking some required fields - it has some required fields, but also has removals, and needs the rest of the fields to remain optional. The TypeScript advanced typing docs seemed promising, but still didn't quite deal with a usecase like this.

I'm still rather new to TypeScript of this depth, and haven't found this usecase in SO yet (apologies if my few dozen searches just missed such an answer). I found things that were similar, but limited to either optional properties with removal of a few or optional properties with requiring a few, but nothing that combined them all together into one that could specify required fields, excluded fields, and leave the rest optional. I just found stuff like:

I would like to have a method that works something like this to create my Component interface:

export type MyComponentInterface = OptionalRequiredRemoved<MyBaseInterface, 'prop1' | 'prop2', 'prop3'>;

Hopefully that makes sense. Any help is appreciated. Thanks!

cjn
  • 1,331
  • 1
  • 16
  • 22

1 Answers1

2

Ok, I eventually figured it out for my feature. The lack of response & low question views doesn't indicate a high value, but maybe someone else out there will benefit from it someday so here goes.

Here's the custom util function I built to be able to specify my Component input fields from an external class that gets generated into the app:

/**
 * Create a new type based on a base type by defining whether the base type properties are optional (default), required, or removed in the new type.
 * All base interface fields are assumed optional unless specified otherwise to be required or removed.
 * Format:
 *   OptionalRequireRemove< BaseInterface, <BAR|SEPARATED|REQUIRED|FIELD|NAME|STRINGS>, <BAR|SEPARATED|REMOVED|FIELD|NAME|STRINGS> >
 *
 * Example:
 *   MyBaseInterface( foo: string, bar: string, baz, string, buzz: string, bam: string )
 *   export type MyNewInterface = OptionalRequireRemove<MyBaseInterface, 'foo' | 'bar' , 'bam'>    // making custom type from base type
 *     MyNewInterface looks like:
 *       MyNewInterface(foo!: string, bar!: string, baz?: string, buzz?: string)  // required: foo/bar; removed: bam; optional: [all others]
 */
export type OptionalRequireRemove<T, K extends keyof T, J extends keyof T> = Omit<Partial<T>, J> & Pick<T, K>;

Here's how it works

I ended up composing a solution from the TypeScript utils classes consisting of a combination of Omit/Partial/Pick. Here's how they work (high level):

  • Partial: Make all fields optional from a selected class (T) or set of fields.
  • Omit: From a class (or set of fields), specify some fields (J) to leave off.
  • Pick: From a set of fields/class, require these fields.

Our function takes a base "T" class, then some arguments of fields to Require (first arg because most likely), and finally some arguments of things to Remove (last because least likely).

The function call actually (surprisingly) ended up like I had hoped for in the original post. For convenience, here's what was desired:

export type MyComponentInterface = OptionalRequiredRemoved<MyBaseInterface, 'prop1' | 'prop2', 'prop3'>;

To understand the new function OptionalRequiredRemoved type that we export, look at the custom type util and follow this flow:

  1. Start at the Partial. We take the base T generated class (MyBaseInterface from my example usage) and using Partial make ALL of its fields OPTIONAL. At this point, our example class would look like this:
    MyBaseInterface: {
      prop1?: string;
      prop2?: string;
      prop3?: string;
      prop4?: string;
      prop5?: string;
      prop6?: string;
    }
    
  2. Then that all-optional-fields acts as the base new "type" that we will Omit some fields from - the J fields from our third argument ('prop3' from my example usage) in the function call. At this point, our type now looks like:
    MyBaseInterface: {
      prop1?: string;
      prop2?: string;
      prop4?: string;
      prop5?: string;
      prop6?: string;
    }
    
  3. Finally, we will specify which fields we "Pick" as required. These fields are the 2nd argument K in our function ('prop1' | 'prop2' from my example usage). Those will be combined with the type we've been building up. Our final type now looks like:
    MyBaseInterface: {
      prop1: string;
      prop2: string;
      prop4?: string;
      prop5?: string;
      prop6?: string;
    }
    

The TypeScript Playground is a great place to see this example and try things out on your own. I found IntelliJ better for building the function, but I found TypeScript Playground better for verifying it.

And this is what was needed for the Component type. If that didn't solve your usecase, hopefully it gets you closer to figuring it out like I did. You can do it!

cjn
  • 1,331
  • 1
  • 16
  • 22