What is the reason for the nullish coalescing operator failing to function as a typeguard in TypeScript?

Introduced in Typescript 3.7, the nullish coalescing operator seems to be a perfect type guard for scenarios like the following:


const fs = (s: string) => s
const fn = (n: number) => n

let a: string | null | undefined
let b: number | null | undefined

const x = (a ?? null) && fs(a)
const y = (b ?? null) && fn(b)

However, running this code on the typescript playground, it raises alerts for both 'a' and 'b' parameters passed into the 'fs' / 'fn' functions:

https://i.sstatic.net/ywI5M.png Further experimentation revealed that the issue extends beyond just the nullish coalescing operator. I'm struggling to understand when typescript can utilize something as a typeguard and when it cannot. Check out some examples.

The last two lines are particularly perplexing to me. Although the expressions assigned to 'x7' and 'x8' appear to be identical, TypeScript acknowledges the typeguard in one but not the other:


const fs = (str: string) => str
const create = (s: string) => s === 's' ? 'string' : s === 'n' ? null : undefined
const a: string | null | undefined = create('s')
const b: string | null | undefined = 's'
let x
if (a !== null && a !== undefined) {
    x = a
} else {
    x = fs(a)
}
const x1 = a !== null && a !== undefined && fs(a)
const x2 = a !== null && a !== void 0 && fs(a)
const x3 = (a ?? null) && fs(a)
const x4 = (b ?? null) && fs(b)
const x5 = a !== null && a !== undefined ? a : fs(a)
const something = a !== null && a !== undefined
const x6 = something ? a : fs(a)
const x7 = something && fs(a)
const x8 = (a !== null && a !== undefined) && fs(a)

I'm unsure if TypeScript is simply unable to apply the typeguard for some reason or if there might be a bug in the system. Is there a guideline for when TypeScript can implement a typeguard and when it cannot? Or could there be another reason causing these examples to fail compilation?

On a side note, employing a user-defined type guard works flawlessly, but it would be ideal not to have to include additional runtime code for the typeguard to function.

Answer №1

After spending a considerable amount of time attempting to outline the mechanical explanation behind why specific expressions such as expr1 || expr2 && expr3 function as type guards in certain scenarios but not in others, I found myself with several pages of content that still didn't cover all the instances provided in your examples. If you are interested, you can reference the code implemented for expression operators in microsoft/TypeScript#7140.


A broader explanation for why these limitations exist: when we, humans, encounter a value of a union type, we have the ability to analyze it by considering what would occur if the value were narrowed down to each member of that type within the entire scope where the value is present. If our code behaves correctly for every case analysis, then it will behave appropriately for the full union. This decision-making process is likely influenced by our concern for the behavior of the code or some cognitive processes that cannot be replicated by a compiler.

Theoretically, the compiler could engage in this type of analysis incessantly for every conceivable union-typed expression it encounters. We might refer to this as "automatic distributive control flow analysis," and it would usually result in producing the desired type guard behavior. However, the drawback lies in the fact that it would demand more memory and processing time than what is reasonable, possibly surpassing human capabilities due to the escalating resource requirements caused by additional union-typed expressions. Algorithms with exponential running times do not lead to efficient compilers.

There have been moments when I desired the capacity to signal to the compiler that a particular union-typed value in a specific scope should be analyzed in this manner. I even proposed the concept of "opt-in distributive control flow analysis" (referenced in microsoft/TypeScript#25051), although implementing this would necessitate substantial development efforts and deviate from TypeScript's goal of supporting JavaScript design patterns without placing undue emphasis on control flow analysis.

Hence, TypeScript language designers opt for heuristics that conduct such analyses within limited scopes to accommodate conventional and idiomatic JavaScript coding styles. If constructs like (a ?? null) && fs(a) aren't deemed conventional or idiomatic enough by the language creators (which involves subjective and empirical considerations based on real-world code samples), and their implementation would significantly impact compiler performance, then it is unlikely that the language would incorporate them.

Examples include:

  • UPDATE FOR TS4.4: The issue raised in microsoft/TypeScript#44370 addresses the ability to "save" the outcome of a type guard into a constant (similar to the something example) for future use. This suggestion remains open for reconsideration, with concerns regarding its viable implementation raised by a language architect due to potential performance challenges. While this approach may align with idiomatic practices, effective execution poses hurdles.

  • microsoft/TypeScript#37258: Advocating for support for consecutive type guards involving narrowings applied to multiple related variables simultaneously. Despite its closure as overly complex, as avoiding an exponential-time algorithm would entail constraining checks to a limited quantity, which wouldn't yield significant benefits overall. The recommendation from those overseeing the language: integrate more conventional checks instead.


This comprehensive response aims to provide clarity on the matter. Best of luck moving forward!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The directive for accepting only numbers is not functioning in versions of Chrome 49.xx.xx and earlier

I have implemented a directive in Angular 6 to allow only numbers as input for certain fields. The directive code is shown below: import { Directive, ElementRef, HostListener } from '@angular/core'; @Directive({ selector: '[NumbersOnly]& ...

Executing a series of promises sequentially and pausing to finish execution

I have been attempting to run a loop where a promise and its respective then method are created for each iteration. My goal is to only print 'Done' once all promises have been executed in order. However, no matter what I try, 'done' alw ...

When the variable type is an interface, should generics be included in the implementation of the constructor?

Here is a code snippet for you to consider: //LINE 1 private result: Map<EventType<any>, number> = new HashMap<EventType<any>, number>(); //LINE 2 private result: Map<EventType<any>, number> = new HashMap(); When the ...

Is it essential to include both the generic type and argument in this TypeScript code?

To better understand the concept, please refer to this code snippet: type Type1 = { num1: number; str1: string }; type Type2 = { num2: number; str2: string }; type Type3 = { num3: number; str3: string }; type Type4 = { num4: number; str4: string }; enum M ...

Is there a way to customize the language used for the months and days on the DatePicker

Is there a way to change the language of the DatePicker (months and days) in Material UI? I have attempted to implement localization through both the theme and LocalizationProvider, but neither method seems to work. Here are some resources I have looked a ...

Utilizing child component HTTP responses within a parent component in Angular: a comprehensive guide

As a newcomer to Angular, I find myself struggling with http requests in my application. The issue arises when I have component A responsible for retrieving a list of IDs that need to be accessed by multiple other components. In component B, I attempted t ...

disable the button border on native-base

I'm attempting to enclose an icon within a button, like so: <Button style={styles.radioButton} onPress={() => { console.log('hdjwk'); }}> <Icon ...

Creating Instances of a Subclass from an Object in TypeScript

Here is the code snippet I am working with: class MyClass { name: string = "myname"; constructor(public action: string) { } } let obj1: MyClass = { action: "act1" }; The code does not compile and the error displayed pertains to the last line: P ...

A guide on how to implement promise return in redux actions for react native applications

I'm using redux to handle location data and I need to retrieve it when necessary. Once the location is saved to the state in redux, I want to return a promise because I require that data for my screen. Here are my actions, reducers, store setup, and ...

"In TypeScript, when something is undefined, it means its value

I am currently working on a class with code to help manage a database. export class DatabaseHelper { public browserID : number; private ConfigID = 17; } Within this class, I am attempting to access the value of ConfigID SetBrowserID() { ...

Strategies for resolving a mix of different data types within a single parameter

Here, I am setting up the options params to accept a value that can either be a single string or another object like options?: string[] | IServiceDetail[] | IServiceAccordion[]; However, when attempting to map these objects, an error is encountered: Prope ...

The parameter 'Value | ValueXY' cannot be assigned to the type 'AnimatedValue<0>'

Recently, I delved into React Native documentation on Animations which can be found here: https://reactnative.dev/docs/animated After reading the docs, I decided to try out some code snippets: import {Animated as RNAnimated} from 'react-native' ...

Unleashing the power of dynamic carousel initiation in Angular using the @ngu/carousel library

Is there an npm package available that supports starting a carousel with a dynamic index in Angular 4 or higher, rather than starting from 0? I've tried using @ngu/carousel, but the myCarousel.moveTo() method doesn't seem to work for this specif ...

The const keyword is not automatically inferred as a const when using conditional types for generic type parameters

Within Typescript, the const modifier can be applied to function type parameters. This ensures that the inferred type matches the literal received with as const. function identity<const T>(a: T){ return a } For example, when using identity({ a: 4 ...

In Next.js, the Typescript compiler does not halt when an error occurs

I am looking to incorporate Next.js with TypeScript into my project. I followed the official method of adding TypeScript to Next.js using npx create-next-app --typescript. Everything seemed fine, but when a TypeScript error occurs (e.g. const st: string = ...

A tip for transferring the value of a binding variable to a method within the template when the button is clicked

I am currently exploring the concept of binding between parent and child components using @Input, @Output, and EventEmitter decorators. This is demonstrated in the HTML snippet below: <h1 appItemDetails [item]="currentItem">{{currentItem}}& ...

Simple and quickest method for incorporating jQuery into Angular 2/4

Effective ways to combine jQuery and Angular? Simple steps for integrating jQuery in Angular2 TypeScript apps? Not sure if this approach is secure, but it can definitely be beneficial. Quite intriguing. ...

"Embrace the powerful combination of WinJS, Angular, and TypeScript for

Currently, I am attempting to integrate winjs with Angular and TypeScript. The Angular-Winjs wrapper functions well, except when additional JavaScript is required for the Dom-Elements. In my scenario, I am trying to implement the split-view item. Although ...

Contrasting type[] (array) with [type] (tuple)

If we imagine having two interfaces: interface WithStringArray1 { property: [string] } interface WithStringArray2 { property: string[] } Let's define variables of these types: let type1:WithStringArray1 = { property: [] } let type2:Wit ...

Typescript sometimes struggles to definitively determine whether a property is null or not

interface A { id?: string } interface B { id: string } function test(a: A, b: A) { if (!a.id && !b.id) { return } let c: B = { id: a.id || b.id } } Check out the code on playground An error arises stating that 'objectI ...