Why can't typescript infer these correlated types?

47 Views Asked by At

Having the following code:

const foo = (flag: boolean) => {
  if (flag) {
    return {
       success: true,
       data: {
         name: "John",
         age: 40
       }
    }
  }

  return {
    success: false,
    data: null
  }
}    

const result = foo(true);

if (result.success) {
   console.log(result.data.name); // TS error: 'result.data' is possibly 'null'
}

Why can't typescript infer that data always exists if flag is set to true?

I know I can fix this by defining a return type for the function, but it'd be good if TS automatically infers it.

2

There are 2 best solutions below

2
Tachibana Shin On

maybe this code fix:

interface ReturnSuccess {
  success: true
  data: { name: string; age: number }
}
interface ReturnFailure {
  success: false
  data: null
}
const foo = <Flag extends boolean, Return extends Flag extends true ? ReturnSuccess : ReturnFailure>(flag: Flag): Return {
  if (flag) {
    return {
       success: true,
       data: {
         name: "John",
         age: 40
       }
    } as Return 
  }

  return {
    success: false,
    data: null
  } as Return 
}    

0
Daniel A. White On

You can do this with as const or create a discriminated union around your success property.

Using as const:

const foo = (flag: boolean) => {
  if (flag) {
    return {
       success: true,
       data: {
         name: "John",
         age: 40
       }
    } as const; // here
  }

  return {
    success: false,
    data: null
  } as const; // here
}    

const result = foo(true);

if (result.success) {
   console.log(result.data.name);
}

Using a union:

type Successful = {
   success: true;
   data: { name: string, age: number }
}

type Failed {
   success: false;
   data: null;
}

type Result = Successful | Failed;

function foo(flag: boolean): Result {
  if (flag) {
    return {
       success: true,
       data: {
         name: "John",
         age: 40
       }
    };
  }

  return {
    success: false,
    data: null
  };
}    

const result = foo(true);

if (result.success) {
   console.log(result.data.name);
}

Why this happens is that just assigning true or false, TypeScript infers that property to a boolean so the data: null case, it can't prove that { success: true, data: null } won't happen. There's also no type information that flag has to relate to the return type. That is where generics help.