Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice when asserting non-null on a value coming from React.useContext (or anywhere else really)

In my React application I'm using the non-null assertion (!) to tell TS that the value coming from useContext hook is not null. It works well and seems to be the textbook use-case for non-null assertion, but with the recommended eslint rules non-null assertion is a warning.

Being new to TypeScript I want to make sure that I'm not missing a common pattern or best practice approach when dealing with these situations. Or maybe there is a different way to type the user object in the example below.

  1. State is defined in a standalone file.
// context.ts
interface State {
  user: User | null; // User type is defined elsewhere, has 'id' and 'email'
}

const state: State = {
  user: null,
};
  1. If user is logged in state.user in defined.
// App.tsx
return state.user ? <Dashboard /> : <Login />;
  1. User can only see the Dashboard if state.user is not null, but I still see the following errors.
// Dashboard.tsx
const { state } = React.useContext(context);

const name = state.user.firstName; // Object is possibly 'null'.
const { id, email } = state.user; // Property 'id/email' does not exist on type 'User | null'.
  1. Logout method sets state = { user: null } so null has to be assignable to the User type.

Anticipating potential answers that don't address my question about best practice/patterns:

  • I know how to disable @typescript-eslint/no-non-null-assertion rule, this isn't the solution I'm looking for.

  • I am aware that I can avoid both the errors and the non-null assertion warning if I use optional chaining in the first example like so: const name = state.user?.firstName;, but this doesn't answer my question. I'd actually prefer non-null assertion in this case, because would be more explicit.

Thanks!

like image 224
Brian Avatar asked Mar 15 '26 12:03

Brian


1 Answers

A couple of options for you:

  1. Explicitly check that user isn't null and throw if it is

  2. Define a "logged-in" state interface and write a hook that returns it after checking that user isn't null

  3. Define a "logged-in" state interface and write a type assertion function, then use that in Dashboard or the hook

The common theme among them is not just asserting that user isn't null, but proving it at runtime so programming errors using state with null are caught proactively and clearly, rather than being random-seeming "Can't use X of null" errors.

1. Explicit check

Since the user can only see the Dashboard component if they're signed in, and when they're signed in the user context member won't be null, you can check that explicitly:

const { state } = React.useContext(context);
const { user } = state;
if (!user) {
    throw new Error(`ERROR: User reached Dashboard with null 'user' in context`);
}
// ...code uses `user`...

That way, TypeScript knows for the rest of the component code that user is not null, and you get a clear error if a programming error results in trying to use Dashboard when user is null.

2. LoggedInState and a hook

Alternatively, you could define LoggedInState:

interface LoggedInState {
    user: User;
}

and write a reusable hook:

function useLoggedInContext(context: ContextType): LoggedInState {
    const { state } = React.useContext(context);
    if (!state.user) {
        throw new Error(`ERROR: User reached logged-in-only component with null 'user' in context`);
    }
    return state as LoggedInState;
}

That still has a type assertion, but in a single reusable location with runtime code above proving that it's a correct assertion.

Then in Dashboard (and elsewhere):

const { state } = useLoggedIncontext(context);
// ...

3. A type assertion function

You can wrap that up in a type assertion function:

function assertIsLoggedInState(state: State): asserts state is LoggedInState {
    if (!state.user) {
        throw new AssertionError(`'state' has a null 'user' member, expected a logged-in state`);
    }
}

Then either use it directly in Dashboard:

const { state } = React.useContext(context);
assertIsLoggedInState(state);
// ...code can use `state.user`, which TypeScript now knows can't be `null`

or use it in that useLoggedInContext hook:

function useLoggedInContext(context: ContextType): LoggedInState {
    const { state } = React.useContext(context);
    assertIsLoggedInState(state);
    return state; // TypeScript knows this is a LoggedInState
}

As a minor tweak to the first one: If you don't need state for anything else, you can combine those first two statements:

const { state: { user } } = React.useContext(context);
// ...

That only declares user, without declaring state. If you have other things from state that you need, you could add them after user.

like image 130
T.J. Crowder Avatar answered Mar 17 '26 02:03

T.J. Crowder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!