Adriaan Marain
Adriaan Marain

Reputation: 354

Can I create a prop that inherits another prop's generic type?

I'm writing a React project using TypeScript, and I want to create a new component that takes a value that is an array of objects, and can create new objects to add to this array. When creating a new object, I want it to start with a blueprint that I define, that already includes some default values for some of the object's fields.

My problem is that I can't seem to get the blueprint to follow the value's generic type. The below (very simplified example) code is perfectly fine according to TypeScript:

type Props<T extends object> = {
  value: Array<T>;
  blueprint: T;
};

function MyComponent<T extends object>({}: Props<T>) {
  return <div></div>;
}

function ParentComponent() {
  return <MyComponent
    value={[{ foo: "bar" }, { foo: "oof"}]}
    blueprint={{ boo: "faz" }} // This doesn't throw a type-error, but I would like it to.
  />
}

I'd like TypeScript to throw a type-error when the blueprint prop doesn't look like an object in the value array. I've tried doing this by using a second generic type that extends or equals the first, but I've not managed to get to a solution.

Upvotes: 2

Views: 1035

Answers (2)

jcalz
jcalz

Reputation: 328292

By default, the compiler will infer T both from the value property and the blueprint property, often as a union of the types inferred from each property. But you want the compiler only to infer T from value and just verify that blueprint matches that. In other words, you want the T in blueprint to be a non-inferential type parameter usage, as suggested in microsoft/TypeScript#14829.

This feature isn't quite officially supported, but there are a few ways to get this behavior. The first one is to "lower the inference priority" of the T in the blueprint property, by intersecting it with the nearly-top type {}:

type NoInfer<T> = T & {};

type Props<T extends object> = {
  value: Array<T>;
  blueprint: NoInfer<T>;
};

Another way is to use conditional type deferral to prevent inference from happening:

type NoInfer<T> = [T][T extends any ? 0 : never];

type Props<T extends object> = {
  value: Array<T>;
  blueprint: NoInfer<T>;
};

In either version, blueprint eventually is evaluated as being type T (or nearly T) but the inference is either delayed or prevented there. Both versions give you the following behavior for your example:

function ParentComponent() {
  return <MyComponent
    value={[{ foo: "bar" }, { foo: "oof" }]}
    blueprint={{ boo: "faz" }} // error!
  // Type '{ boo: string; }' is not assignable to type '{ foo: string; }'.
  />
}

which is what you wanted.


Not sure it's worth doing these workarounds as they may have some observable issues or edge cases in a wider code base. That's probably up to you to decide how badly your use case requires a no-infer. Okay, hope that helps; good luck!

Playground link to code

Upvotes: 3

Ruan Mendes
Ruan Mendes

Reputation: 92274

This is because TypeScript has no way to know what the infer site is for T. So it finds the lowest common denominator in both usages. In your case, Typescript considers your T to be

{foo: string;  boo?: undefined;} |  {boo: string;  foo?: undefined;}

The simplest solution is to specify in your call what type to use:

function ParentComponent() {
  // does not compile
  return <MyComponent<{foo: string}>
    value={[{ foo: "bar" }, { foo: "oof"}]}
  />
}

If you really would like to not have to specify the params from the caller, you'd need some witchcraft to tell the compiler where T is supposed to get its type from. You'd need a helper function that returns another function to generate your Props. This way, Typescript knows that the parameter passed to the first function is the infer site.

function getProps<T extends object>(value: T[]): (blueprint: T) => Props<T> {
   return function(blueprint: T) {
     return {
       value,
       blueprint
     };
   }
}

const doesCompile = getProps([{a: 1}])({a:2});
const doesNotCompile = getProps([{a: 1}])({b: 2});

function ParentComponentWithFunctionCallCompiles() {
  const props = getProps([{a: 2}])({a: 1});
  return <MyComponent {...props} />
}

function ParentComponentWithFunctionCallDoesNotCompile() {
  const props = getProps([{a: 2}])({b: 1});
  return <MyComponent {...props} />
}

You can play with this on stackblitz

I've also asked a similar question before. See Enforce sub-type in array of generics. I decided to answer this question instead of just marking it a dupe because it does require a lot of grokking before understanding it. At least it did for me.

Upvotes: 1

Related Questions