Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional chaining, proper infer in type guard #34974

Closed
maciejsikora opened this issue Nov 7, 2019 · 7 comments · Fixed by #55613
Closed

Optional chaining, proper infer in type guard #34974

maciejsikora opened this issue Nov 7, 2019 · 7 comments · Fixed by #55613
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@maciejsikora
Copy link

maciejsikora commented Nov 7, 2019

TypeScript Version: 3.7.0

Search Terms: optional chaining

Code

type X = {
  y?: {
    z?: string
  }
}
const x: X = {
  y: {
  }
}
// type guard
function isNotNull<A>(x: A): x is NonNullable<A> {
  return x!== null && x!== undefined;
}
// function which I want to call in the result of the expression
function title(str: string) {
  return str.length > 0 ? 'Dear ' + str : 'Dear nobody';
}

isNotNull(x?.y?.z) ? title(x.y.z) : null // type error - object is possibly undefined

The code fix is by adding additional checks or additional temporary variables:

(x?.y?.z && isNotNull(x.y.z)) ? title(x.y.z) : null
// or
const tmp = x?.y?.z
isNotNull(tmp) ? title(tmp) : null

Expected behavior:
TypeScript is able to infer that optional chaining was used as an argument, what means that typeguard is checking the whole chain.

Actual behavior:
TypeScript needs a code change in order to understand that optional chaining already checked other values from being null | undefined.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Nov 8, 2019
@RyanCavanaugh
Copy link
Member

This is a little tricky; TS sees the x is NonNullable<A> as an opaque treatment of the type of A. As humans we know that if x?.y?.z isn't undefined/null, then x.y can't be undefined/null, but TS doesn't have a way to apply that knowledge through the x is NonNullable<A> check because x is NonNullable<A> is just some generic type that happens to produce some value, not a special check that changes the types of the subexpressions that produced its type argument.

@MartinEneqvistRegionSkane

I have a similar issue with type guards.

This compiles:

type Person = { name: string; }

const getName = (person?: Person): string => {
  return typeof person?.name === 'string' ? person?.name : '';
};

But this does not:

type Person = { name: string; }

const isString = (value: any): value is string => {
  return typeof value === 'string';
};

const getName = (person?: Person): string => {
  return isString(person?.name) ? person?.name : '';
};

// Type 'string | undefined' is not assignable to type 'string'.
//   Type 'undefined' is not assignable to type 'string'.

I would have expected these examples to be equivalent.

Is there another way to write the isString type guard function so that this works?

@lo1tuma
Copy link

lo1tuma commented Sep 27, 2021

I’ve encountered the same problem.
@ShuiRuTian will this be fixed by #38839 as well?

@ShuiRuTian
Copy link
Contributor

@lo1tuma Sorry, but no.

As Ryan said, type guard is not a part of control flow analytics, so type narrowing is not working.

Here is a compare:

type X = {
    y?: {
        z?: string
    }
}
const x: X = {
    y: {
    }
}
// type guard
function isNotNull<A>(x: A): x is NonNullable<A> {
    return x !== null && x !== undefined;
}

if (x?.y?.z != null && x?.y?.z != undefined) {
    x.y.z // this works after 38839
}

if(isNotNull(x?.y?.z)){
    x.y.z // // However, this not
}



@stabai
Copy link

stabai commented Jul 10, 2022

TypeScript knows how to do this properly if you directly check that the chained variable is not null/undefined. It only fails if you use a type guard to check it.

This works:

if (animal?.breed?.size != null) {
  return animal.breed.size;
} else {
  return undefined;
}

This doesn't:

if (!isNil(animal?.breed?.size)) {
  return animal.breed.size;   // Error: Object is possibly 'undefined'. (2532)
} else {
  return undefined;
}

function isNil(value: unknown): value is undefined | null {
    return value == null;
}

Here is a playground link demonstrating this.

I don't know what TypeScript is doing internally, but the fact that one works and the other doesn't is what makes this feel like a bug to me rather than a type system limitation.

@RyanCavanaugh RyanCavanaugh added Experience Enhancement Noncontroversial enhancements and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Aug 31, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Aug 31, 2022
@RyanCavanaugh
Copy link
Member

This might be more tractable to fix now that NonNullable has a simpler definition

@kbrilla
Copy link

kbrilla commented Oct 16, 2022

What about ternary conditions? Would it be possible to also narrow those like here

function getBreedSizeWithTenary(animal: Animal): string|undefined {
    if (animal ? animal.breed ? animal.breed.size : undefined : undefined) {
        return animal.breed.size;
    } else {
        return undefined;
    }
}

here we also know that if the condition in if is true then animal is present, animal.breed is present so animal.breed.size should be valid and not required to use animal!.breed!.size or animal?.breed?.size right?

playground

@RyanCavanaugh any hope for that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants