4

I'm trying to define re-usable React components that accept a React.Context field through a prop. They may not need the entirety of the properties available in the parent's context and, given the desire for reuse, may be encapsulated in Providers with different Context structures (but the same core properties needed by the reused sub component). For instance a parent provider higher in the tree may define the Context type like so:

type SuperSet = {
    x: number,
    y: number,
    z: number
}

let superSet = {x: 1, y: 2, z: 3}
const SuperSetContext = React.createContext<SuperSet>(superSet)

const SuperSetProvider = (props) => {
  return (
    <SuperSetContext.Provider value={superSet}>
      ...
      {/* Arbitrarily deep nested component in the tree, most likely in a different file*/}
      <SubComponent Context={SuperSetContext} />
    </SuperSetContext.Provider>
  );
}

The SubComponent should (I believe) be able to define a Context prop with less properties like so

const SubComponent: React.FunctionComponent<{
  Context: React.Context<{x: number, y: number}>
}> = ({ Context }) => {
  const { x, y } = useContext(props./Context);

  return (<div>{x + y}</div>)
}

Or via Pick<>

Context: React.Context<Pick<SuperSet, 'x' | 'y'>>

However either way the above SubComponent causes a type error when the prop is assigned within the Provider

<SubComponent Context={SuperSetContext} />
Type 'Context<SuperSet>' is not assignable to type 'Context<SubSet>'.
  Types of property 'Provider' are incompatible.
    Type 'Provider<SuperSet>' is not assignable to type 'Provider<SubSet>'.
      Types of parameters 'props' and 'props' are incompatible.
        Type 'ProviderProps<SubSet>' is not assignable to type 'ProviderProps<SuperSet>'.ts(2322)
test.tsx(26, 3): The expected type comes from property 'Context' which is declared here on type 'IntrinsicAttributes & { Context: Context<SubSet>; } & { children?: ReactNode; }'

I created a Typescript Playground to test it without jsx but it occurs regardless of using jsx. Additionaly I don't see the same behavior with naive generic classes/functions.

So is there a way to define the SubComponent's Context definition with a subset or Context properties OR a different paradigm to accomplish the same design and escape this particular typing mismatch?

Zachary
  • 85
  • 2
  • 11

2 Answers2

4

If you declare you own interface that extends React.Context<T>, Typescript will accept it.

interface MyContext<T> extends React.Context<T> {} // this does the trick

const SubComponent: React.FunctionComponent<{
  // use MyContext instead of React.Context
  Context: MyContext<{x: number, y: number}> 
}> = ({ Context }) => {
  const { x, y } = React.useContext(Context);

  return <div>{x + y}</div>;
};


// Now pass a SuperSet

type SuperSet = {
  x: number;
  y: number;
  z: number;
};
let superSet = { x: 1, y: 2, z: 3 };

const SuperSetContext = React.createContext<SuperSet>(superSet);
const SuperSetProvider = props => {
  return (
    <SuperSetContext.Provider value={superSet}>
      {/* No TS error! */}
      <SubComponent Context={SuperSetContext} />  
    </SuperSetContext.Provider>
  );
};

you can check it out in this sandbox

To be honest, I can't explain what causes the difference in behavior.

Ori Marron
  • 96
  • 3
  • This doesn't seem to work for me, I wonder if it's a Typescript version/config thing that enables it in CodeSandbox. The same interface extending method in a Typescript playground (v3.8/v3.7/v3.3) still produces the error as well as in my local environment. Thanks for the working codesandbox though! – Zachary Apr 16 '20 at 20:37
2

In this case you should use a generic component (SubComponent), since TS is right here: you can't assume React.Context<SubSet> is a subset of React.Context<SuperSet> because they're boxed types. Using a simple generic component, you can work around this specifying what you really want: that the context type should be a subset:

function SubComponent<T extends { x: number, y: number }>(props: {
  context: React.Context<T>
}) {
  const { x, y } = React.useContext(props.context);

  return (<div>{x + y}</div>)
}

You can see full example in the playground: Playground Link

leonardfactory
  • 3,353
  • 1
  • 18
  • 25