What allows the execution of "T[keyof T] extends Function" in TypeScript specifically for Strings?

Lately, I've been experimenting with type changes and I find myself puzzled when encountering code like the following:

type DeepReadonly<T> = {
  readonly [k in keyof T]: T[k] extends Function?T[k]:DeepReadonly<T[k]>
}

// Let's define X1 to test

type X1 = {a: string};
type A = DeepReadonly<X1>

// It's unbelievable that it works. Shouldn't there be a loop?

// I mean if you give the DeepReadonly function the string type, shouldn't it loop endlessly?

Could someone please explain how TypeScript manages to make this code function...

Your help is greatly appreciated!

Answer №1

When looking at the DeepReadonly<T> type:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends Function ? T[K] : DeepReadonly<T[K]>
}

it falls under the category of being a homomorphic or structure-preserving mapped type. This means that it explicitly maps over the keys of another type using in keyof. For more information, you can refer to this resource on "homomorphic mapped types".

In the context of Microsoft's TypeScript documentation (microsoft/TypeScript#12447), they are referred to as "isomorphic" mapped types. Here is an excerpt for clarity:

A mapped type written as { [P in keyof T]: X }, where T is a type parameter, is known as an isomorphic mapped type because it produces a type with the same shape as T. It states that when a primitive type replaces T in an isomorphic mapped type, the result will be that primitive type.


Hence, since string is a primitive type, DeepReadonly<string> directly evaluates to string without going into the details of evaluating

string[K] extends Function ? string[K] : DeepReadonly<string[K]>
.

This explains why your type A only delves one level deep before coming to an end:

type X1 = { a: string };
type A = DeepReadonly<X1>;
/* type A = {
    readonly a: string;
} */

The above explanation adequately answers the initial query. However, it is noteworthy that TypeScript has the capability to represent recursive data structures without causing any issues during type instantiation:

interface Tree {
  value: string;
  children: Tree[]
}

type B = DeepReadonly<Tree>
/* type B = {
    readonly value: string;
    readonly children: readonly DeepReadonly<Tree>[];
} */

Even though the Tree type and resultant B type are defined recursively, there are no pitfalls encountered. Although conceptually there may be a loop, the compiler does not encounter one.

Hence, even if DeepReadonly<string> wasn't a homomorphic mapped type, it would still lead to a valid but complex recursive type where all apparent members of string would be enumerated and altered:

type NonHomomorphicDeepReadonly<T> = keyof T extends infer KK extends keyof T ? {
  readonly [K in KK]: NonHomomorphicDeepReadonly<T[K]>
} : never;

type C = NonHomomorphicDeepReadonly<string>;
/* type C = {
    readonly [x: number]: ...;
    readonly [Symbol.iterator]: {};
    readonly toString: {};
    readonly charAt: {};
    readonly charCodeAt: {};
    readonly concat: {};
    readonly indexOf: {};
    readonly lastIndexOf: {};
    readonly localeCompare: {};
    ... 34 more ...;
    readonly padEnd: {};
} */
type D = C[0][0][0][0];
/* type D = {
    readonly [x: number]: ...;
    readonly [Symbol.iterator]: {};
    readonly toString: {};
    readonly charAt: {};
    readonly charCodeAt: {};
    readonly concat: {};
    readonly indexOf: {};
    readonly lastIndexOf: {};
    readonly localeCompare: {};
    ... 34 more ...;
    readonly padEnd: {};
} */

While this might not be ideal, it illustrates the rationale behind why homomorphic mapped types behave as they do with primitive types. Ultimately, it results in an acceptable type structure.

Playground link to code

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

Differences between Typescript static methods and functions defined outside of classesWhen comparing Types

What distinguishes a static class method from a function defined outside a class in TypeScript, especially when visibility is not a concern? While there are differences in visibility from other classes and files, what factors should be considered when dec ...

Context API is failing to work in components that use children when the version is v16.6.0 or higher

Currently, I am utilizing the latest context API of React (v16.6.0 or higher) by specifying the public static contextType inside the component that consumes the context. Everything works smoothly unless the component declaring the Provider directly include ...

What is the process for listening to custom events in Angular 4 components that have been loaded using routing?

In the app.component.html file <li routerLinkActive="active current"> <a [routerLink]="['/stats']"> Cluster stats </a> </li> When we route to the DisplayAllStatsComponent, how can we ...

Discovering the proper method for indicating the type of a variable in the middle of a statement

In my code, there was a line that looked like this: let initialPayload = this.db.list("/members").snapshotChanges() as Observable<any[]> But then I changed it to this: let initialPayload = this.db.list("/members").snapshotChanges ...

Adding innerHTML content to tooltip title using typescript in an Angular project

I have encountered an issue while trying to display HTML content inside a tooltip element's title attribute. The HTML content is not rendering as expected and appears as text instead. Let me outline the structure of my Angular project: library.comp. ...

Having trouble with VueJS ref not preventing the default form action on submit?

Within my <script> tag, I currently have the following code: render(createElement) { return createElement("form", {ref: "formEl" , on: {submit: this.handleSubmit} }, [ <insert create form inputs here> ]); } handleSubmit(e) { ...

Tips for sending a file rather than a json object in nextjs

Is there a way to send a file from either route.ts or page.ts, regardless of its location in the file-system? Currently, I am using the following code in my back-end python + flask... @app.route("/thumbnail/<string:filename>") def get_file ...

Taunting a specific occurrence inside a group

Good evening, I am currently in the process of developing tests for the TypeScript class shown below. My goal is to create a test that ensures the postMessage method of the internal BroadcastChannel is called. However, I am facing difficulties in setting ...

I continue to encounter the same error while attempting to deliver data to this form

Encountering an error that says: TypeError: Cannot read properties of null (reading 'persist') useEffect(() => { if (edit) { console.log(item) setValues(item!); } document.body.style.overflow = showModal ? "hidden ...

A guide on converting JSON to TypeScript objects while including calculated properties

I have a standard JSON service that returns data in a specific format. An example of the returned body looks like this: [{ x: 3, y: 5 }] I am looking to convert this data into instances of a customized TypeScript class called CustomClass: export class ...

When merging interfaces and classes, Typescript does not verify property initialization

When creating a class like the following: class Dog { a: string; b: string; c: string; } The TypeScript compiler will throw an error stating that properties a, b, and c are not initialized. However, if we take a different approach like this: i ...

What is the best way to prevent the output folder from appearing in the import statements for users of my package?

I have a project written in Typescript that consists of multiple .d.ts files. I would like to package this project as an npm module and utilize it in another project. In the second project, my goal is to be able to import modules like so: import {Foo} fr ...

Utilizing feature flags for Angular modules to enable lazy loading

Can we dynamically change the lazy loaded module based on a specific flag? For instance, loading module A if the flag is active and module B otherwise. The crucial aspect is that both modules should use the same path. Approach #1 - dynamic loadChildren() ...

Trouble navigating through an index of elastic data? Learn how to smoothly scroll with Typescript in conjunction with

I'm currently using the JavaScript client for Elasticsearch to index and search my data, but I've encountered an issue with using the scroll method. Although I can't seem to set the correct index, I am confident in my technique because I am ...

Angular loop using an HTTP GET request is producing garbled information

Currently, I have a loop that includes an http GET request. This is the sample loop code: for (let index = 0; index < datas.length; index++) { let car = datas[index].smiles; console.log('or--> ' + car); this.subscr = this.CarServ ...

Ways to immediately display an uploaded image as the background on a canvas

Using TypeScript, I am attempting to set an uploaded image as the background of a canvas. However, I am facing an issue where the image only loads properly after the user has uploaded it two times. How can I ensure that the image has finished loading befor ...

Error: The current call does not match any existing overloads - TypeScript, NextJS, styled-components

I'm facing an issue while trying to display icons in the footer of my website. The error message "Type error: No overload matches this call" keeps appearing next to my StyledIconsWrapper component, causing Vercel deployment to fail. Despite this error ...

Looking for assistance in correctly identifying types in react-leaflet for TypeScript?

Embarking on my 'first' project involving react-scripts-ts and react-leaflet. I am attempting to create a class that should be fairly straightforward: import {map, TileLayer, Popup, Marker } from 'react-leaflet'; class LeafletMap exte ...

Having trouble with your Typescript *.ts files?

Having trouble understanding why the command tsc *.ts isn't functioning correctly. The error message TS6053: File '*.ts' not found keeps appearing. Any suggestions on how to compile all the .ts files within a directory? Thank you! ...

Downloading PDF files on IOS while using Angular often results in the PDF opening in the same

I'm currently utilizing file-saver within my Angular application to retrieve a PDF generated from the backend. The library functions smoothly on desktop and Android devices, but I'm encountering issues with downloading files on iOS. Contrary to w ...