1

I am using CreateContext in Typescript and I can see why there is a problem with the code but I cannot work out how to resolve it. Its a basic use of typesafe TX to make state and (useReducer) dispatch available in a component hierarchy

The sandbox: https://codesandbox.io/s/typescript-usereducer-todo-lk391?file=/src/App.tsx

The context interface:

export interface ContextInterface {
  state: AppState;
  dispatch: (action: ActionType) => void;
}

Using the interface to createContext (I think it has to be a partial because createContext wont take zero args)

const TodoContext = createContext<Partial<ContextInterface>>({});

The context is initialised in my parent component:

  let [state, dispatch] = useReducer(reducer, initialState);
  ...
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
  ...

When I useContext TS reports Cannot invoke an object which is possibly 'undefined'.ts(2722), however the console.log executes as expected.

  let { dispatch } = useContext(TodoContext);
  console.log("dispatch", dispatch);

Is there a way to correctly define the Context object and remove the error?

Laurence Fass
  • 1,614
  • 3
  • 21
  • 43
  • Or just give it an initial value, why would you use a `Partial` type – Dennis Vash Jan 26 '22 at 09:49
  • I gave it a Partial as I cant initilise until I have a dispatch function from useReducer. The other option was to initialise with undefined (its wont take nil arguments) which creates a different set of problems. – Laurence Fass Jan 26 '22 at 11:12

2 Answers2

3

You can wrap your useContext call in a custom hook that will take care of the type issues:

  1. first change the definition of TodoContext to include an optional empty state

const TodoContext = createContext<ContextInterface | undefined>(undefined);
  1. create a custom hook that throws an informative error on how this hook should be used
export const useTodoContext = (): ContextInterface => {
  const context = useContext(TodoContext)
  if (!context) {
    throw Error('useTodoContext must be used inside TodoContext.Provider')
  }
  return context
}

then you can use this hook without issues.

This approach scales well to all types of context data.

thedude
  • 9,388
  • 1
  • 29
  • 30
2

Fundamentally, the issue is that by using Partial, you've made all properties of the context type optional, so they might be undefined (from a type perspective), and you need to allow for that if you go that route.

According to the createContext documentation, the default is only used when there's no relevant provider, so instead of using Partial, you could just include a default context object that always throws:

const TodoContext = createContext<ContextInterface>({
    state: {/*...mocked AppState stuff...*/},
    dispatch: (action: ActionType) => {
        throw new Error(`Default context used unexpectedly, check you have a provider`);
    }
});

That way, TypeScript won't think dispatch may be undefined.

If there aren't any reasonable defaults for the AppState properties, you could use a Proxy that throws an error similar to the one above on any property access, so that attempts to use context.state.x provide the same clear error.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875