Exploring the rationale behind infinite recursion in Typescript

Take a look at this unique typescript 4.2 snippet I stumbled upon (you can find the playground here):

type BlackMagic<T> = { [K in keyof T]: BlackMagic<T[K]> }

declare const foo: BlackMagic<{q: string}>;
declare const str: BlackMagic<string>;
declare const num: BlackMagic<12>;

This code is blowing my mind. How does TypeScript manage to handle this without getting stuck in an infinite recursion? What's even more fascinating is that when you inspect the types of variables like str and num, it resolves them to just basic types like string and 12. It's like magic!

Answer №1

If your type includes BlackMagic<T> with the same T, you would encounter infinite recursion. However, in this scenario, we are utilizing the utility type BlackMagic with a different value T[K].

type BlackMagic<T> = { [K in keyof T]: BlackMagic<T[K]> }

This definition indicates that BlackMagic<T> is an object where the keys correspond to the keys of T and the values represent a mapped version of the values of T using BlackMagic.


In the examples provided—str and num, the BlackMagic type does not have any impact.

When T is a primitive type rather than an object, BlackMagic<T> is equivalent to T itself. This aligns with the standard behavior of utility types, specifically mapped types. For instance, Partial<12> simply results in 12. Refer to the FAQ Common "Bugs" That Aren't Bugs for further clarification.

Mapped types defined as { [ K in keyof T ]: U } with T as a type parameter are classified as homomorphic mapped types, signifying that the mapping preserves the structure of T. When a primitive type is assigned to type parameter T, the mapped type resolves to the same primitive type.


foo: BlackMagic<{q: string}>
involves mapping, albeit restricted to one level deep. Consequently, BlackMagic<{q: string}> transforms into {q: BlackMagic<string>}. Subsequently, considering that BlackMagic<string> equals string, the type progresses to {q: string}.

Answer №2

According to Linda's explanation, the absence of infinite recursion occurs because a homomorphic mapped type, when applied to a primitive type, results in that same primitive type. However, it is worth exploring the outcome with a non-homomorphic mapped type. To create a non-homomorphic type, you can define an identity type I such that

I<'key1' | 'key2'> = 'key1' | 'key2'
, using a conditional type:

type I<T> = T extends infer S ? S : T

By extracting keys from I<keyof T> rather than keyof T, you can establish a straightforward non-recursive and non-homomorphic type:

type NonHomomorphicMap<T> = {[K in I<keyof T>]: 42}

This type can then be applied to various other types as shown below:

type TestObject = NonHomomorphicMap<{q: string}> // {q: 42}
type TestString = NonHomomorphicMap<string>      // {[x: number]: 42, toString: 42, charAt: 42, ...}
type TestNum = NonHomomorphicMap<12>             // {toString: 42, toFixed: 42, toExponential: 42, toPrecision: 42, valueOf: 42, toLocaleString: 42}
type TestFun = NonHomomorphicMap<() => number>   // {}

In cases where the input is a string or 12, the resulting type transforms into an object type containing keys for all methods associated with the respective types along with an [x: number] index signature for strings.

It's interesting to note that functions are not classified as primitive types but rather operate as keyless objects, hence

NonHomomorphicMap<() => any>
evaluates to {}. This behavior also applies to homomorphic mapped types such as BlackMagic<() => any>, illustrating that BlackMagic does not equate to identity.

You can also construct a recursive non-homomorphic type similar to BlackMagic, but a Normalize type is necessary to fully assess the complete type on hover:

type Normalize<T> =
  T extends Function
  ? T
  : T extends infer S ? {[K in keyof S]: S[K]} : never

Subsequently, a non-homomorphic equivalent to BlackMagic can be defined as:

type BadMagic<T> = Normalize<{
    [K in I<keyof T>]: K extends keyof T ? BadMagic<T[K]> : never
}>

In addition to employing Normalize and I, an extra conditional K extends keyof T is included to ensure TypeScript recognizes that the output from applying I still indexes

T</code, although this has no impact on functionality.</p>
<p>Applying <code>BadMagic
to the sample types yields:

type TestObject = BadMagic<{q: string}> // {q: {[x: number]: {[x: number]: BadMagic<string>,...}, toString: {}, charAt: {}, ...}}
type TestString = BadMagic<string>      // {[x: number]: {[x: number]: {[x: number]: BadMagic<string>, ...}, toString: {}, charAt: {}, ...}, toString: {}, charAt: {}, ...}
type TestNum = BadMagic<12>             // {toString: {}, toFixed: {}, toExponential: {}, toPrecision: {}, valueOf: {}, toLocaleString: {}}
type TestFun = BadMagic<() => any>      // {}

The majority of recursion terminates at method properties that resolve into {}. However, when inspecting BadMagic<string>, some level of "infinite" recursion becomes apparent as the [x: number] property on strings recursively refers back to itself. Essentially:

BadMagic<string> = {
  [x: number]: {
    [x: number]: {
      [x: number]: BadMagic<string>, // "infinite"
      ...
    },
    ...
  },
  toString: {},
  charAt: {},
  ...
}

Explore the TypeScript playground.

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 AuthGuard (Guard decorator) is unable to resolve its dependencies according to Nest

My AuthGuard is responsible for checking the JWT token in controllers. I am trying to use this Guard in controllers to verify authentication, but I encountered the following error: Nest cannot resolve dependencies of the AuthGuard (?, +). Please ensur ...

How can I extract a specific data value from a JSON file in Ionic 2?

Here is the JSON data: [ { "id": 1, "label": "saw", "total": "100" }, { "id": 2, "label": "saw1", "total": "300" }, { "id": 3, "label": "saw2", "total": "400" } ] Below is my Typescript code snippet: this. ...

invoke a method from a different class within the same component file

I am facing a situation where I have 2 classes within the same component.ts file. One class is responsible for embedding the Doc blot, while the other class serves as the main component class. I need to call a function that resides in the component class f ...

Embed the div within images of varying widths

I need help positioning a div in the bottom right corner of all images, regardless of their width. The issue I am facing is that when an image takes up 100% width, the div ends up in the center. How can I ensure the div stays in the lower right corner eve ...

TypeScript and Next.js failing to properly verify function parameters/arguments

I'm currently tackling a project involving typescript & next.js, and I've run into an issue where function argument types aren't being checked as expected. Below is a snippet of code that illustrates the problem. Despite my expectation ...

A Vue component library devoid of bundled dependencies or the need for compiling SCSS files

My current challenge involves the task of finding a way to publish our team's component library. These components are intended to be used by various internal applications within our organization. I have specific requirements: The library must be acc ...

I'm seeking a way to access the input element within the InputGroup component of BlueprintJs in my

The BluePrintJS React InputGroup component offers a convenient user interface for modern applications. However, it does not provide direct access to the internal <input> element. There are two primary reasons why I need to access the input element: ...

Rearrange the provided string in a particular manner

Looking to transform a string like '12:13:45.123 UTC Sun Oct 17 2021' into 'Sun Oct 17 2021 12:13:45.123 UTC' without calling slice twice. Is there a more elegant and efficient way to achieve this? Currently using: str.slice(18)+&apo ...

What is the reason for TypeScript's refusal to accept this task?

In my attempt to create a type that can be A, B, or an object with a key containing an array of 2 or more items that are either A, B, or another similar object (thus allowing for recursive definition). This is the solution I came up with: type A = { p ...

Setting options using the form group in dropdowns in Angular can greatly enhance the user experience

I have created a FormGroup called holidayform and set it up as follows: holidayform: FormGroup; this.holidayform = this.fb.group({ title: ['', [Validators.required]], entryDate: ['',], }) this.holidayform.patchValue ...

Try logging in again if an error occurs

I've encountered some failing tests that we suspect are caused by network drops. To address this problem, I have modified my login method to retry after an error is detected. I would also like to have the number of retry attempts displayed in the cons ...

retrieve the router information from a location other than the router-outlet

I have set up my layout as shown below. I would like to have my components (each being a separate route) displayed inside mat-card-content. The issue arises when I try to dynamically change mat-card-title, as it is not within the router-outlet and does not ...

Guide on retrieving URL from backend to iframe with Angular Material selection change

https://i.sstatic.net/mWBb5.png This is an .html page <iframe width="100%" height="100%" src="{{url}}" frameborder="0" allowfullscreen></iframe> Here is the code for a .ts file this.url = 'https://www.youtube.com'; Whenever a sel ...

I am encountering an issue regarding the 'endpoint' property within my environment.ts file while working on an Angular 17 project

My goal is to incorporate the property endpoint from my environment.ts file into my service: export const environment = { production: false, endpoint: 'http://localhost:3000/api/cabin/' }; This snippet showcases my service: import {Injectabl ...

Display the highlighted text within the input field

Is there a library available that can create an input field with a suggestions dropdown for Male and Female where typing "M" will highlight the letters "ale," allowing for autopopulation when clicked or tabbed? The same functionality should apply for typin ...

How can I access DOM elements in Angular using a function similar to the `link` function?

One way to utilize the link attribute on Angular 2 directives is by setting callbacks that can transform the DOM. A practical example of this is crafting directives for D3.js graphs, showcased in this pen: https://i.sstatic.net/8Zdta.png The link attrib ...

Monitor constantly to determine if an element is within the visible portion of the screen

For a thorough understanding of my query, I feel the need to delve deeper. While I am well-versed in solving this issue with vanilla Javascript that is compatible with typescript, my struggle lies in figuring out how to invoke this function throughout th ...

Efficiently implementing state and dispatch for useReducer in TypeScript with React

I'm encountering an error in my current setup. The error message reads: 'Type '({ team: string | null; } | { team: string | null; } | { ...; } | { ...; } | { ...; } | Dispatch<...>)[]' is missing the following properties from t ...

Creating a dynamic dropdown menu where the available options in one select box change based on the selection made in another

Looking at the Material-UI Stepper code, I have a requirement to create a select element with dynamic options based on the selected value in another select within the same React component. To achieve this, I wrote a switch function: function getGradeConte ...

Attempting to assign the object retrieved from the interface as the new value for window.location.href

I encountered an issue where the error message Type MyInterface' is not assignable to type 'string' popped up. Although I comprehend the problem, finding a suitable solution has proven to be challenging. MyInterface solely returns one item, ...