Short answer:
const MyContext = createContext<MyContextValueType | null>(null);
See: Context without default value.
Long answer:
(1.) createContext is defined as:
function createContext<T>( defaultValue: T ): Context<T>
So createContext returns Context<T>, where T is inferred from the given defaultValue.
If defaultValue is null, then the return type of createContext is Context<null>:
const MyContext: Context<null> = createContext(null);
(2.) Context is defined as:
interface Context<T> { Provider: Provider<T>; Consumer: Consumer<T>; }
So, given that Context<null>, the provider and consumer will be
Provider<null> and Consumer<null>.
Therefore, you can not pass any value other than null to MyContext.Provider.
(MyContext.Provider is Provider<null> and expects only null and nothing else)
const MyContext: Context<null> = createContext(null);
// ...
<MyContext.Provider value={ myActualStateValue }> {* <-- ERROR: Provider expects only `null` *}
General solution:
Define the type of the defaultValue explicitly to accept both null and the actual state,
with createContext<MyContextValueType | null>.
- This will return
Context<MyContextValueType | null>,
- therefore the provider is
Provider<MyContextValueType | null>,
- therefore
MyContext.Provider accepts both, MyContextValueType and null.
Additionally, the consumer should always handle the null value:
const MyConsumingComponent: React.FC = function(){
const myStateValue: MyContextValueType | null = useContext( MyContext );
console.debug( myStateValue?.state ); // <-- no error because of the "?"
if( myStateValue === null ){
return null;
}
console.debug( myStateValue.state ); // <-- no error because of `myStateValue === null`
return <p>...</p>
};
If type assertions are preferred:
If you know for sure that the null value will never reach your consumers, and you prefer a solution with type assertion for some reason,
this type assertion should probably better be done inside the consumers, not in the provider.
That is because in existing consumers you do know that they are only used when the state is available, but in the provider you
do not know for sure if some day later another consumer will be created that will see the null value, and adapting the provider then can easily be forgotten.
A note about the reasoning
MyContext.Provider and MyContext.Consumer can, in principle, used independently. So you may use the consumer before a proper value was passed to the provider (even if you don't actually do that).
Therefore, the context should always have a "default" value, which always needs to be handled in consumers.
The consumer may
- display a fallback,
- or cause a component to return
null,
- or just decide that this is an error,
- or something else,
but in any case, the default value should be handled explicitly in some way, because it can (in principle) happen.
Solution for your example:
import { type Context, createContext, type Dispatch, useReducer } from "react";
type MyStateValueType = string;
interface MyContextValueType {
state: MyStateValueType,
dispatch: Dispatch<MyStateValueType>,
}
const MyContext: Context<MyContextValueType | null> = createContext<MyContextValueType | null>(null);
const MyStoreProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const [ state, dispatch ] = useReducer( (state: MyStateValueType)=> state, 'some value' );
return (
<MyContext.Provider value={{ state, dispatch }}> {/* <-- works: Provider accepts `null` and MyStateType */}
{ children }
</MyContext.Provider>
);
}