What determines the narrowing of a type when it is defined as a literal versus when it is returned from a function?

I'm really trying to wrap my head around why type narrowing isn't working in this scenario.

Here's an example where name is successfully narrowed down:

function getPath(name: string | null): "continue" | "halt" {
  if (name) {
    return "continue";
  }

  return "halt";
}

function doSomethingWithName(name: string): number {
  return name.length;
}

const name: string | null = "john";

const path = getPath(name);

if (path === "continue") {
  // All good
  doSomethingWithName(name);
}

And here's an example where name fails to be narrowed:

function getPath(name: string | null): "continue" | "halt" {
  if (name) {
    return "continue";
  }

  return "halt";
}

function doSomethingWithName(name: string): number {
  return name.length;
}

function getName(): string | null {
  return "john";
}

const name = getName();

const path = getPath(name);

if (path === "continue") {
  // TypeError: Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' not assignable to type 'string'.
  doSomethingWithName(name);
}

I must be missing a key piece on how type narrowing is supposed to function. Why does it make a difference whether name is assigned as a literal or as the result of a function, especially when the condition that should narrow its type comes after the initial assignment?

Edit: Thank you for all the responses. I now see that my assumption regarding explicit types causing TypeScript to view even literals as string | null was incorrect. This leads me to another question: why doesn't getPath properly narrow down the type of name? If it returns 'continue', shouldn't name be inferred as a string?

Answer №1

Through static analysis, the compiler can automatically determine that :

const myName: string | null = "john";

should actually be represented as myName: string.

This type of analysis and optimization is possible because since myName is never reassigned, it can only hold a value of type string.

Answer №2

Within your code:

const name: string | null = "john";

TypeScript infers that name is of type string

The compiler utilizes Control flow based type analysis

The narrowed type of a local variable or parameter at a specific point in the code is determined by tracing back from that point, adjusting the variable's type as needed based on type guards and assignments.

  • For a local variable, the initial type is undefined.
  • For a parameter, the initial type is the declared parameter type.
  • For an outer local or global variable, the initial type is the declared type of the variable.
  • A type guard refines the variable's type in the code path following the guard.
  • An assignment of a value of type S to a variable of type T narrows the variable's type to T intersected with S after the assignment.
  • When multiple paths lead to a certain point, the narrowed type of a variable there is the union of narrowed types along those paths.

The calculated type T intersected with S is:

  • If T is not a union type, the result is T.
  • If T is a union type, the result is the union of constituent types in T to which S can be assigned.

Note that assigning a known string value narrows the variable to string, but assigning a string | undefined keeps it as string | undefined (its original declaration type).

Update regarding follow-up question

The compiler does not track the outcomes of control flow analysis across function boundaries, assuming that called functions do not impact variable types. Refer to microsoft/TypeScript#9998

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

Conditional generic type in return type with Typescript

How can I condition the generic return type when a value is not present? type Foo = {}; class Bar<P extends Foo> { static make<P extends Foo>(a?: P): Bar<P> { return new Bar(); } } Bar.make() // returns Bar<Foo> ...

Develop an enhancement for the Date object in Angular 2 using Typescript

Using the built-in Date type, I can easily call date.getDate(), date.getMonth()...etc. However, I am looking for a way to create a custom function like date.myCustomFunctionToGetMonthInString(date) that would return the month in a string format such as &a ...

What is the best way to set the generics attribute of an object during initialization?

Below is the code that I have: class Eventful<T extends string> { // ↓ How can I initialize this attribute without TypeScript error? private eventMap: Record<T, (args?: any) => void> = ? } Alternatively, class Eventful<T extends st ...

There is no matching overload for this call in Angular. Error code: TS2769

Having trouble identifying the issue in this TypeScript code snippet. Error reported on line 5, ".subscribe((response: { Token: string }) => {". login() { this.httpClient .post('http://localhost:4000/signin', this.loginForm.value) ...

Tips for neatly wrapping a class constructor

Currently, I am experimenting with code to create a more streamlined Angular Dialog initializer. This initializer should be passed a constructor function along with its arguments in a type-safe manner. The current implementation works, but it is challengi ...

Sort the array by the elements in a separate array

Here is a filters array with three values: serviceCode1, serviceCode2, and serviceCode3. ['serviceCode1', 'serviceCode2', 'serviceCode3'] I have another array with approximately 78 records that I want to filter based on the a ...

What is the correct way to exclude and remove a portion of the value within an object using TypeScript?

The function useHider was created to conceal specific values from an object with the correct type. For example, using const res = useHider({ id: 1, title: "hi"}, "id"), will result in { title: "hi" } being returned. Attempting ...

Creating QR codes from raw byte data in TypeScript and Angular

I have developed a basic web application that fetches codes from an endpoint and generates a key, which is then used to create a QR Code. The key is in the form of an Uint8Array that needs to be converted into a QR Code. I am utilizing the angularx-qrcode ...

Navigating to the tsconfig.json file based on the location of the file being linted

In my monorepo, each package currently contains a .eslintrc.cjs file with the following setup: Package-specific ESLint Configuration const path = require('path') const ts = require('typescript') const OFF = 0 const WARN = 1 const ERROR ...

The method of having two consecutive subscribe calls in Angular2 Http

Can the Subscribe method be called twice? I am attempting to create an API factory that stores data in the factory and allows different components to use that data for each AJAX call. The factory: export class api { result = []; constructor (p ...

The assignment of Type Program[] to a string[] is not valid

I am working with a class that contains information about different programs. My goal is to filter out the active and inactive programs, and then retrieve the names of those programs as an array of strings. Below is the structure of the Program class: ex ...

What steps should I take to enable TypeScript IntelliSense to recommend correct suggestions within discriminated unions?

I am working on creating a checkbox UI component based on a design in Figma. The outline variant is specified to only be compatible with the large size, while the solid variant can be used with all sizes. As a result, I am trying to build an interface whe ...

Deleting a key from a type in TypeScript using subtraction type

I am looking to create a type in TypeScript called ExcludeCart<T>, which essentially removes a specified key (in this case, cart) from the given type T. For example, if we have ExcludeCart<{foo: number, bar: string, cart: number}>, it should re ...

Guide to extracting the JSON array from a JSON object with Angular

In my angular application, I have made a call to the API and retrieved a JSON object in the console. However, within this JSON object, there are both strings and arrays. My task now is to extract and parse the array from the object in the console. The JSO ...

Ways to set a default value for a function that returns an unknown type in TypeScript

In my code, I have a customizedHook that returns a value of type typeXYZ || unknown. However, when I try to destructure the returned value, I encounter an error TS2339: Property 'xyz' does not exist on type 'unknown', even though the da ...

Retrieve a specific item from the ngrx/store

My Reducer implementation in my Angular 2 app is designed to store state items related to price offers for Financial Instruments, such as stocks and currencies. This is the implementation of my Reducer: export const offersStore = (state = new Array<Of ...

Implementing reduce for filtering and mapping in TypeScript: A comprehensive guide

I encountered a problem with my code that I need help fixing. Here is a simple example: interface employer { name: string; age: number; } const arr: employer[] = [{name:'Amy',age:18},{name:'Bob',age:20}]; l ...

Error: Angular 2 - Node - gulp | Unable to locate module .component

I'm in the process of developing a complete TypeScript application with Node.js in TypeScript that is intended to be used with Angular 2 and built using Gulp as the build tool. The Gulp task successfully compiles all files from /src to /dist, convert ...

Tips for determining if an HTMLElement has already been created

One issue I'm facing is with a third party component that emits an "onCellEdit" event and passes a cell element as a parameter. My goal is to automatically select the entire text in the input element generated inside this cell when the event occurs. ...

Guide to adding a loading spinner into your Angular project

I've been attempting to incorporate a spinner into my application, but unfortunately, the spinner isn't showing up. Despite checking the console and terminal for errors, there doesn't seem to be any indication as to why the spinner is not a ...