'Assert that value from Context is not null in child component?

I'm building a Next.js/React app with TypeScript. I have a UserContext in pages/_app.js which provides a user: User | null object to all child components.

I also have a ProtectedRoute component to wrap pages which shouldn't be accessible without being logged in (it consumes the user from UserContext and redirects if user === null).

However, the type of user that children of ProtectedRoute see is still User | null. It should just be User, since we can't access a protected route without being logged in. Is there a TypeScript or React design pattern that would accomplish this?

I looked at non-null assertions with !, but those are default-disabled by ESLint and I don't want to do that in every child of ProtectedRoute. I also looked at type guards, but not sure if there's a way to avoid doing that in every child component.

Ideally, the type of user from UserContext would change to just User instead of User | null if the component consuming the context is a child of ProtectedRoute.

Thank you!

EDIT: ProtectedRoute code:

function ProtectedRoute({ children }: { children: JSX.Element }) {
  const { user } = useSupabase();  // This has type `User | null`
  const router = useRouter();
  useEffect(() => {
    if (!user) {
      router.push("/login");  // There may be a more Next-specific way to do this...
    }
  }, [user, router]);
  return user ? children : null;
}


Solution 1:[1]

If you're positive that the component will never be rendered in a logged-out state, then you can use an assertion function in a custom hook to transform the type.

Note that you're making a guarantee by throwing an error if the condition can't be met, and that this will crash your component if you use it incorrectly.

TS Playground

// You didn't show what these are or where they come from:
interface User {}
declare function useSupabase (): { user: User | null };
declare function useRouter (): string[];

type NonNullableProperties <T, Key extends keyof T> = {
  [K in keyof T]: K extends Key ? NonNullable<T[K]> : T[K];
};

function assert (expr: unknown, msg?: string): asserts expr {
  if (!expr) throw new Error(msg ?? 'Value is falsy');
}

function useSupabaseAuthenticated (): NonNullableProperties<ReturnType<typeof useSupabase>, 'user'> {
  const result = useSupabase();
  assert(result.user, 'Invalid state');
  return result as NonNullableProperties<typeof result, 'user'>;
}

function ProtectedRoute ({ children }: { children: JSX.Element }) {
  const { user } = useSupabaseAuthenticated(); // is now just User
  // const router = useRouter();
  // useEffect(() => {
  //   if (!user) router.push("/login");
  // }, [user, router]);
  // return user ? children : null;
  return children;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1