TypeScript: How to refine a type where one of it's properties is probably null

Viewed 23

I'm trying to understand how to write a function that would refine my type and then return it.

It works for the passed object itself and within the function body but not outside. Furthermore what I find odd is that even if it works within the body of the function, it stops working on the return statement itself.

My expectation would be that TypeScript correctly returns a refined / narrowed down type, checked properties included not just the main object.

What am I doing wrong? Does TypeScript require more help, maybe some generic return type?

Example:

 
type Referrer = { id: string }

type User = {
    id: string;
    referrer: Referrer | null;
} | null;


function checkUserHasReferrer(user: User) {
  if (!user) {
      throw Error('No User');
  }

  if (!user.referrer) {
    console.log(user.referrer) // referrer correctly narrowed to `referrer: null`
    throw Error('No Referrer');
  }
  const referrer = user.referrer; // referrer correctly narrowed to `referrer: Referrer`
  console.log(user.referrer).     // same here, `referrer: Referrer`
  

  return user; // <- Here however it's back to `referrer: Referrer | null`
}


let initialUser: User = null;
const user = checkUserHasReferrer(initialUser);
console.log(user.id);           // <-- knows User is not `null`
console.log(user.referrer.id);  // However it doesn't know referrer is not `null`

TypeScript playground

2 Answers

You need to use assertion functions

type Referrer = { id: string }

type User = {
  id: string;
  referrer: Referrer | null;
}



function notNull(user: unknown): asserts  user {
  if (!user) {
    throw Error('No User');
  }
}

declare const user: User | null


notNull(user)
notNull(user.referrer)

console.log(user.id);
console.log(user.referrer.id);  // ok

Playground

When you need to throw an error in TypeScript, usually such function is a good candidate to be assertion function


Or, you can use custom typeguard functions

type Referrer = { id: string }

type User = {
  id: string;
  referrer: Referrer | null;
}

const isValidUser = (user: User | null): user is User & { referrer: Referrer } =>
  Boolean(user && user.referrer)

declare const user: User

if (isValidUser(user)) {
  console.log(user.id);
  console.log(user.referrer.id);  // ok
}

Playground

This is currently a known limitation of TypeScript. See here or here. TypeScript does correctly narrow the type of the property but the type of the parent is not modified, so it remains just User.

One possible workaround without type assertions would be to spread the object when returning it.

return {
    ...user,
    referrer
};

This changes the return type of the function to

{
    referrer: Referrer;
    id: string;
}

Playground

Related