Skip to content

Promise.catch null vs null as null #44913

Closed

Description

@bluelovers

Bug Report

🔎 Search Terms

🕗 Version & Regression Information

  • This is a crash
  • This changed between versions ______ and _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________
  • I was unable to test this on prior versions because _______

⏯ Playground Link

Playground link with relevant code

💻 Code

Version 4.4.0-dev.20210705

image

export function f1()
{
	return Promise.resolve(1).catch(e => null)
}

export function f2()
{
	return Promise.resolve(1).catch(e => null as null)
}

// @ts-expect-error
f1().then(v => v.split('')) // <====== should is `number` but `any`

// @ts-expect-error
f2().then(v => v.split(''))
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2019",
    "jsx": "preserve",
    "moduleResolution": "node",
    "allowJs": false,
    "allowUmdGlobalAccess": false,
    "sourceMap": true,
    "inlineSourceMap": false,
    "inlineSources": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "strict": false,
    "strictBindCallApply": true,
    "strictNullChecks": false,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noImplicitOverride": true,
    "preserveConstEnums": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "noErrorTruncation": true,
    "removeComments": true,
    "traceResolution": false,
    "newLine": "lf",
    "esModuleInterop": true,
    "importHelpers": true
  }
}

🙁 Actual behavior

// @ts-expect-error
f1().then(v => v.split('')) // <====== should is `number` but `any`

🙂 Expected behavior

// @ts-expect-error
f1().then(v => v.split('')) // <====== should is `number`

Activity

  1. fatcerberus commented on Jul 8, 2021

    @fatcerberus

    I think this happens because in the absence of a type annotation, null gets inferred as any, even under strictNullChecks.

    let x = null;  // type: any
    let y = null as null;  // type: null
  2. rbuckton commented on Oct 23, 2023

    @rbuckton
    Contributor

    I think this happens because in the absence of a type annotation, null gets inferred as any, even under strictNullChecks.

    let x = null;  // type: any
    let y = null as null;  // type: null

    That's not quite true. When outside of --strictNullChecks there are two null types: A non-widening null type and a widening null type. The widening null type is returned when null is used as an expression, while the non-widening null type is returned when null is used as a type. At any point where we widen (such as when we widen "foo" to string), the widening null is either removed from any union it is contained in, or is converted to any, while the non-widening null is only removed from unions if there are any other non-widening types. When inside of --strictNullChecks, there is only ever one null type, which is the non-widening null type. The same is also true for undefined.

    The example you show here isn't quite correct, as the type you see when you hover over x isn't the inferred type, it is the type resulting from control flow analysis:

    // @strictNullChecks: true
    let x = null;
    //  ^?  let x: any
    
       x;
    // ^?   let x: null

    Playground

    In the example from the OP, you can see that both number and null are preserved in --strictNullChecks:

    // @strictNullChecks: true
    export function f1()
    {
    	return Promise.resolve(1).catch(e => null)
    }
    
    export function f2()
    {
    	return Promise.resolve(1).catch(e => null as null)
    }
    
    // @ts-expect-error
    f1().then(v => v.split('')) // <====== should is `number` but `any`
    //        ^? (parameter) v: number | null
    
    // @ts-expect-error
    f2().then(v => v.split(''))
    //        ^? (parameter) v: number | null

    Playground

    The reason why you see any is due to null being the only possible return value of .catch. As a result, it widens to any as the type escapes the body. You can see that here, though I had to replace the arrow functions with function expressions so I could have something to point the ^? comment at:

    // @strictNullChecks: false
    export function f1()
    {
    	return Promise.resolve(1).catch(function(e) { return null; })
            //                              ^? (local function) (e: any): any
    }
    
    export function f2()
    {
    	return Promise.resolve(1).catch(function (e) { return null as null; })
            //                              ^? (local function) (e: any): null
    }
    
    // @ts-expect-error
    f1().then(v => v.split('')) // <====== should is `number` but `any`
    //        ^? (parameter) v: any
    
    // @ts-expect-error
    f2().then(v => v.split(''))
    //        ^? (parameter) v: number

    Playground

    As a result, when constructing the resulting Promise type returned from catch in f1, the result is number | any, which is reduced to any.

    Currently, this is a limitation of running outside of --strictNullChecks, but I'm considering whether there are any changes we can make here.

  3. rbuckton commented on Aug 16, 2024

    @rbuckton
    Contributor

    I've put up #59661, which reports an error due to the implicit widening from null to any.

  4. added
    BugA bug in TypeScript
    Fix AvailableA PR has been opened for this issue
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    Fix AvailableA PR has been opened for this issue
    on Aug 16, 2024
  5. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issueRescheduledThis issue was previously scheduled to an earlier milestone

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions