What methods does TypeScript use to compare infinite recursive types for equality?

How does TypeScript handle equality checks for infinite recursive types?

Consider the following example:

// LL is equivalent to L unfolded once
type L = [] | {item: number, next: L}
type LL = [] | {item: number, next: ({item: number, next: LL} | [])}

// Assigning an L to an LL is allowed
declare const L1: L
const LL1: LL = L1

// Assigning an LL to an L is allowed
declare const LL2: LL
const L2: L = LL2

type Interassignable<T, U> = T extends U ? U extends T ? true : never : never

declare const check: Interassignable<L, LL>
const x: true = check // OK

Link to playground

This raises two key questions:

  1. How does TypeScript verify that an L can be assigned to an LL (and vice versa).

  2. How does TypeScript verify that L extends LL (and vice versa)

The solution likely involves caching recursive types to prevent endless validation, but further details are needed.

I am seeking a pseudocode or descriptive explanation of the algorithm applicable to this scenario.

Answer №1

Your insight into the termination process is correct. Typescript indeed includes a mechanism to limit recursion. The key function for compatibility checking is isRelatedTo in checker.ts. This function yields either False, Unknown, Maybe, or True. While True and False indicate clear relations, Maybe is of particular interest. It signifies uncertainty when comparing two types during the process. To address this, the compiler maintains an array of relations currently under consideration.

Let's examine a simplified recursive example:

type L = { next: L}
type LL = { next: ({ next: LL})}

declare const L1: L
const LL1: LL = L1

How does the compiler establish that L1 can be assigned to LL1?

Q-1. Is L1 assignable to LL1?
Q-2. This is only possible if L.next and LL.next are compatible.
Q-3. Can L be assigned to { next: LL}?
Q-4. This requires compatibility between L.next and { next: LL}.next.
Q-5. Is L1 assignable to LL1?
A-5. Assuming they are compatible at this point, return Maybe.
A-4. As their types may be compatible, return Maybe.
A-3. Since no properties definitively clash and one property is marked as Maybe, return Maybe.
A-2. Considering potential compatibility, return Maybe.
A-1. With no definite incompatibilities found, they are deemed assignable.

An (overly) simplified pseudocode version of the code would look like this:

interface Type { id: string, properties: Map<string, { type: Type}> }

enum Ternary {
  True = -1,
  False = 0,
  Maybe = 3
}

function checkTypeRelatedTo(source: Type, target: Type){
  let maybeRelated: string[]
  return isRelatedTo(source, target) != Ternary.False;

  function isRelatedTo(source: Type, target: Type): Ternary {
    const relationId = source.id + "," + target.id;
    if(maybeRelated.indexOf(relationId) != -1) {
      return Ternary.Maybe
    }
    maybeRelated.push(relationId);
    const result = structureRelatedTo(source, target);
    maybeRelated.pop();
    return result;
  }

  function structureRelatedTo(source: Type, target: Type): Ternary{
    let result = Ternary.True;
    for(const prop of target.properties.keys()) {
      result &= isRelatedTo(source.properties.get(prop)!.type, target.properties.get(prop)!.type)
      if(!result){
        return Ternary.False
      }
    }
    return result;
  }
}

Playground Link

Incorporating additional members and unions doesn't fundamentally alter this algorithm; it simply adds layers of complexity. Unions are deemed compatible if any constituent from one union aligns with any constituent of the other union. Member compatibility also has minimal impact. If even one member is potentially compatible, the entire type is considered potentially compatible, even if all other properties are definitely compatible.

Answer №2

When discussing algorithms, the documentation here explains that TypeScript's structural type system determines compatibility based on having the same members.

Compatibility between types x and y is determined by checking if y has all the properties of x.

To assign y to x, each property in x is checked to find a corresponding compatible property in y.

Now let's revisit an example:

// LL is equivalent to L unfolded once
type L = [] | { item: number, next: L }

/**
 * This is because it goes one level deeper, but the types are essentially the same
 */
type LL = [] | { item: number, next: ({ item: number, next: LL } | []) }
  1. L and LL can both be empty arrays or objects with two properties.

Here's a simple demonstration:

type A = [] | {}
type B = [] | {}

let a: A = []
let b: B = {}
a = b // a can also be an empty object
b = a // b can also be an empty array
  1. L and LL can also be objects with properties item and next.

So, when the TS compiler encounters L, it asks:

TS: Hey L, can I treat you as an object with 2 properties?

L: Certainly, because I am typed as an object with two properties (item & next).

TS: Hello, LL, can I consider you as an object with properties item and next?

LL: Absolutely, that's my type. You can even see me as an empty array too.

TS: Okay, L and LL, would it be alright if I treat you as an empty array?

L,LL: No problem at all 😊

Due to the structural type system, both types are treated equally.

This is my interpretation of this algorithm.

UPDATE for recursion: I highly recommend referring to the documentation for a detailed explanation about recursive type aliases.

Prior to TypeScript 3.7, there were limitations around referring to the defined type inside the type itself.

With TypeScript 3.7, such limitations have been lifted. The compiler now defers resolving type arguments at the top level of a type alias, enabling complex patterns.

Before TS 3.7, developers had to use interfaces along with types to create recursive types.

For instance:

type ValueOrArray2<T> = T | ArrayOfValueOrArray<T>;
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray2<T>> {}

You can experiment with this concept on the Playground.

In essence, TS creates an alias when indirectly referring to the type itself (recursive references).

To further understand how recursive types operate, review the tests in the TypeScript repository.

I lack deep knowledge of the TS repo to conduct a thorough step-by-step analysis of this algorithm.

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

Exploring the world of Typescript TSX with the power of generic

Introducing JSX syntax support in Typescript opens up new possibilities. I have an expression that functions smoothly with traditional *.ts files, but encounters issues with *.tsx files: const f = <T1>(arg1: T1) => <T2>(arg2: T2) => { ...

Assign a predetermined value to a dropdown list within a FormGroup

I have received 2 sets of data from my API: { "content": [{ "id": 1, "roleName": "admin", }, { "id": 2, "roleName": "user", }, { "id": 3, "roleName": "other", } ], "last": true, "totalEleme ...

Creating an interactive questionnaire

Looking for insights on the following situation: I'm working with a survey structure that consists of questions and answers: questions: Question[]; answer: Answer[]; Where Question is defined as: export class Question { idQuestion: string; ...

The debate between using backticks and colons in TypeORM queries

Lately, I've been crafting queries utilizing backticks const firstUser = await connection .getRepository(User) .createQueryBuilder("user") .where(`user.id = '${id}'`) .getOne(); However, in the typeorm documentatio ...

Is there a way to define a type once and then use it for both a function and a component in React using TypeScript?

I have a types.ts file where I define the Redux action types, a Screen.tsx file for a component that uses the actions, and an Actions.ts file where the actions are defined. I want to find a way to declare the action type only once and then use it across bo ...

Redirect to login not working in Angular auth guard

Seeking assistance with troubleshooting an issue within my app. I am attempting to implement a feature where, if the user is not logged in and tries to access any URL of the app, they should be redirected to the login page. I have set up an auth guard, bu ...

Splitting Angular modules into separate projects with identical configurations

My Angular project currently consists of approximately 20 different modules. Whenever there is a code change in one module, the entire project needs to be deployed. I am considering breaking down my modules into separate projects for individual deployment. ...

Using Node.js and Typescript to implement a chain of logical AND operations with an array of any length

Setting: I am developing a versatile function that determines a boolean outcome based on logical AND operations. However, the function is designed to handle various types of objects and arrays, each requiring different conditions. Currently, my code look ...

Could directly destructure parameters upon typing be an option?

Is there a way to create destructured parameters with optional typing in TypeScript? For instance, I am trying to make the following function callable without requiring any parameters. const func = ({param}: {param?: boolean}) => {}; Currently, TypeS ...

Converting a JSON object into a different format using TypeScript

Looking for advice on how to efficiently convert JSON data into a specific format without hardcoding any values like "root" or "Amount". I want to create a reusable function that can be used in various scenarios. Currently, I am working with TypeScript and ...

Developing a custom HTTP request class in Angular 2 and its exporting capabilities

I have created a simple HTTP requests class using Angular 2: http-handler.ts import {Http, Headers, RequestOptions} from 'angular2/http' import {Injectable} from 'angular2/core'; import 'rxjs/add/operator/toPromise'; @Injec ...

Importing Modules in Angular: Explicit vs Implicit Approach

My current setup includes a SharedModule that is imported by every other module. This means that some modules have an implicit import to the SharedModule because they import other modules that already import it. I'm curious if this could potentially i ...

What is the best way to display the arrows on a sorted table based on the sorting order in Angular?

I need assistance with sorting a table either from largest to smallest or alphabetically. Here is the HTML code of the section I'm trying to sort: <tr> <th scope="col" [appSort]="dataList" data-order ...

Learn how to dynamically pass a click event from a div to a checkbox with delegation

I need assistance with a scenario where clicking on a button within a component triggers another click event on a specific checkbox located in a different div within the same component. Essentially, I want to programmatically simulate a click on the checkb ...

What is the best way to retrieve the subclass name while annotating a parent method?

After creating a method decorator to log information about a class, there is a slight issue that needs addressing. Currently, the decorator logs the name of the abstract parent class instead of the effectively running class. Below is the code for the deco ...

Maintain hook varieties during implementation of array deconstruction

I have been developing a straightforward hook to export animation helper and element reference. import { gsap } from 'gsap'; import { useRef } from 'react'; export function useTween<R extends gsap.TweenTarget>(vars: gsap.TweenVar ...

Inhibit unselected choices when a user exceeds choosing a certain number of options

I want users to be able to select a maximum of 3 options from a list. If a form has four options, once the user selects three, the fourth should become disabled. Here is the setup I have: import { Component, Input, ViewChild, OnInit, AfterViewI ...

The field 'token' is not found in the PropsWithChildren<WithUrqlProps> type but it is mandatory in the type '{ token: string; }'

Encountering an error at (ChangePassword) in the export default withUrqlClient(createUrqlClient)(ChangePassword): The argument of type 'FunctionComponent<{ token: string; }> & { getInitialProps?(context: NextPageContext): { token: string; } | ...

Converting a cast method into a function in Typescript

With some experimenting on WebRTC, I found that the ondatachannel callback takes a function, but I'm wondering if it's possible to use a method of a Typescript class instead. Can someone please assist me with this? export class MainComponent imp ...

Whenever I attempt to launch my Angular frontend on VS Code, I encounter an issue stemming from a PrimeNG checkbox error

Error: src/app/auth/login/login.component.html:20:58 - error NG8002: The property 'binary' cannot be bound to 'p-checkbox' because it is not recognized. If 'p-checkbox' is an Angular component and contains the 'binary&ap ...