question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

TypeScript fails to narrow out `undefined` via `typeof` check on generic indexed access

See original GitHub issue

Bug Report

🔎 Search Terms

undefined narrowed 2322

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about ‘undefined’ (including via Find on Page)

⏯ Playground Link

Playground link with relevant code

💻 Code

type StarStats = {
    mass: number;
    surfaceTemperature: number;
    planets: number;
}
type PlanetStats = {
    mass: number;
    moons: number;
    orbitalPeriod: number;
    orbitalInclination: number;
}
interface PlanetaryBodiesMap {
    'Planet' : PlanetStats;
    'Star' : StarStats;
}
const getStatFromSet = function<
    TN extends keyof PlanetaryBodiesMap,
>(
    statSet : Partial<PlanetaryBodiesMap[TN]>,
    statName : keyof typeof statSet,
) : number {
    const potentialResult = statSet[statName];
    //Adding redundant `&& potentialResult !== undefined` to the conditional,
    //the error message changes between v4.7.4 and v4.8.0-beta
    if(typeof potentialResult !== 'undefined') {
        potentialResult;
        //Hover above to see: Partial<PlanetaryBodiesMap[TN]>[keyof PlanetaryBodiesMap[TN]]
        //Error ts(2322) below:  Type 'PlanetaryBodiesMap[TN][keyof PlanetaryBodiesMap[TN]] | undefined' is not assignable to type 'number'.
        //Where does that extra `| undefined` come from after having narrowed the type of the `const`?
        return potentialResult;
    } else {
        return 0;
    }
}

🙁 Actual behavior

TypeScript complains that a constant value can be undefined within a conditional block where that is already checked for and excluded.

🙂 Expected behavior

  1. Inside a conditional which can only be entered if a constant type is not undefined, TypeScript excludes | undefined from the possible types of the constant.
  2. When showing the type of a constant as X in an error message about how that can’t be assigned to Y, the type X should be the same as the type shown when hovering over the type. In this example, they differ by | undefined, which matters. In the unsimplified version where the type is more complex, the type of the constant shown on hover is very linguistically different from the type shown in the error, making this issue harder to debug.

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
wbtcommented, Aug 31, 2022

>3-year-old #32365 looks related and may have something to do with an underlying reason; it’s possible that fixing that issue could also fix this one. However, I think it’s quite possible this issue could also be fixed without fixing that one by focusing on the elimination of ‘undefined’ from the type of any constant that is inside a conditional eliminating undefined from its type.

0reactions
wbtcommented, Nov 28, 2022

Here is what I suspect is another example of the same issue, though it can be broken out to a separate issue if others think it’s not a duplicate:

//In the motivating example, these types are way more complex than simple constant strings.
interface AnimalSounds { Cat: 'meow'; Dog: 'woof'; Duck: 'quack';}
type AnimalSound = AnimalSounds[keyof AnimalSounds]; //"meow" | "woof" | "quack"
export const callerFn = function<A extends keyof AnimalSounds> (
    animalTypeName: A,
    sonicEnvironment: Partial<AnimalSounds> //cannot be restricted to require A here
) {
    const sound = sonicEnvironment[animalTypeName];
    if (typeof sound === 'undefined') {
        throw new Error('Could not find sound in environment.');
    }
    //At/after this line, 'sound' should be narrowed to EXCLUDE the 'undefined' type possibility.
    //i.e. sound should be Exclude<Partial<AnimalSounds>[A], undefined>, but the exclusion isn't working.
    //You can move the error and DRY up casting by using a narrowing const, but it's still an error:
    const soundNarrowed: Exclude<Partial<AnimalSounds>[A], undefined> = sound; //Type 'undefined' not assignable
    //Error in first parameter of next line:
    //Argument of type 'Partial<AnimalSounds>[A]' is not assignable to parameter of type 'AnimalSounds[A]'.
    //Type '"meow" | undefined' is not assignable to type '"meow"'.
    //Type 'undefined' is not assignable to type '"meow"'.ts(2345)
    calledFn<AnimalSounds[A]>(sound, toUpperCaseTyped(sound));
    //In the line below but NOT above, the 'undefined' possibility is correctly narrowed out:
    calledFnNoGenericOneParam(sound);
}
const calledFn = function<S extends AnimalSound>(sound: S, loudSound: Uppercase<S>) {/*...*/};
//Dropping the generic doesn't work in context,
//because multiple types in the function are derived from the generic type parameter.
const calledFnNoGenericOneParam = function(sound: AnimalSound) {/*...*/};
//Bonus issue(#44268): the cast in the return statement of the next line should be automatic & unnecessary:
const toUpperCaseTyped = function<S extends string>(strIn: S) {return strIn.toUpperCase() as Uppercase<S>;};

With this example, the behaviour changed between 4.2.3 and 4.3.5, so that the parameter to calledFnNoGenericOneParam is correctly constrained to exclude undefined; this fix unfortunately did not address the narrowing for the call to calledFn but the PR for it might be informative.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Narrowing - TypeScript
In our section about truthiness narrowing, we wrote a printAll function which was error-prone because it accidentally didn't handle empty strings properly.
Read more >
Checking an optional generic parameter for undefined results ...
The only typeof check I see being affected here is for typeof xxx ... in narrowing between x === undefined and typeof x...
Read more >
Understanding TypeScript generics through lodash functions
Similarly, firstScore is a number (or undefined ) because we called first() with an array of numbers. And just to prove that generics...
Read more >
A complete guide to TypeScript's never type - Zhenghao
Inadmissible parameters in generics and functions. Intersection of incompatible types. An empty union (a union type of nothingness).
Read more >
Managing objects with unknown structures in TypeScript using ...
Unfortunately, the error message is correct, and the Object type accepts primitive values other than null. It might be the case because we...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found