Understanding the typing inference of Symbol.species in Typescript

Imagine I have a customized MyArray<T> class that extends the built-in Array<T> class. How can I type it so that when using

myMap<T>(myArr: MyArray<T>, <Function>)
, the return type is correctly inferred as MyArray<T> rather than Array<T>?

Note: I want this type declaration to be very general, which is why I haven't simply overloaded the map method. This way, I can easily change the signature for the type variable Arr in myArr to Iterable<T>, making it usable with other built-in/custom classes like Set<T> that implement the Iterable Protocol. For now, users need to specify their desired return type as a generic function variable.

class MyArray<T> extends Array<T> { }

declare const myArr: MyArray<number>;

const myArrMapped_ = myArr.map(x => x); // expected MyArray<number>, got number[]

// It's understandable because `Array.prototype.map`'s type signature
// doesn't provide a way to determine what is returned by `MyArray<T>[Symbol.species]`.
// But if I were to create my own `Array.prototype.map`, how could I
// get the typing of `MyArray<T>[Symbol.species]` to ensure it returns
// MyArray<number> in the above example?

import { Expect, Equal } from '@type-challenges/utils';

declare function myMap<T, Arr extends T[], R>(arr: Arr, func: any): any; // myMap

const myArrMapped = myMap(myArr, (x: number) => x);

type test = Expect<Equal<typeof myArrMapped, MyArray<number>>> // how can I make this work?

...

Playground Link

Answer №1

I think there is no need to specify any static properties, as it would require implementing all of them.

If you plan on overriding certain methods from Array.prototype, it's better to only type those specific methods that you are interested in.

import { Expect, Equal } from '@type-challenges/utils';

class MyArray<T> extends Array<T> {

    override map<U>(callbackfn: (value: T, index: number, array: MyArray<T>) => U, thisArg?: any): MyArray<U> {
        return []
    }
}

const instance = new MyArray<number>()

const result = instance.map((elem) => elem * 2)


type test = Expect<Equal<typeof result, MyArray<number>>> // ok

Playground

The issue here is that Symbol.species is a static property and cannot be accessed from an instance. Refer to the source code for more information.

Take a look at the following example:

type Keys = {
    [P in keyof SetConstructor]:P
}
// type Keys = {
//     readonly prototype: "prototype";
//     readonly [Symbol.species]: typeof Symbol.species;
// }

And:

type Keys = {
    [P in keyof Set<number>]:P
}
// type Keys = {
//     add: "add";
//     clear: "clear";
//     delete: "delete";
//     forEach: "forEach";
//     has: "has";
//     readonly size: "size";
//     entries: "entries";
//     keys: "keys";
//     values: "values";
//     [Symbol.iterator]: typeof Symbol.iterator;
//     readonly [Symbol.toStringTag]: typeof Symbol.toStringTag;
// }

Therefore, Symbol.species can only be inferred from the constructor. What does this mean in practice?

const NOT_IMPLEMENTED = null as any;

type ClassType = new (...args: any[]) => any

// if Symbol.species] exists in T -  it is a COnstructor
type Species<T> = T extends {
    readonly [Symbol.species]: infer Constructor;
} ? Constructor : never

interface MyArray<T> {
    [Symbol.iterator](): IterableIterator<T>;

}

interface MyArrayConstructor {
    new(): any
    [Symbol.species]: MyArrayConstructor
}

declare var MyArray: MyArrayConstructor;

const builder = <T extends ClassType>(constructor_: T): Species<T> => NOT_IMPLEMENTED

const set = builder(Set) // SetConstructor
const array = builder(Array) // ArrayConstructor
const myArray = builder(MyArray) //  MyArrayConstructor


type Test1 = Species<typeof MyArray> // MyArrayConstructor
type Test2 = Species<typeof Set>     //  SetConstructor
type Test3 = Species<typeof Map>     //  MapConstructor

Consider this example:

type GetPrototype<T> = T extends { prototype: infer Proto } ? Proto : never

const NOT_IMPLEMENTED = null as any;

type Species<T> = T extends {
    readonly [Symbol.species]: infer Constructor;
} ? Constructor : never

type IteratorElement<T extends Iterable<any>> = T extends {
    [Symbol.iterator](): Iterator<infer Elem>
} ? Elem : never

type Instance = InstanceType<SetConstructor> // Set<unknown>


function map<Elem extends Iterable<any>, Constructor extends Species<Elem> >(array: Elem, cb: (elem: IteratorElement<Elem>) => any): GetPrototype<Elem> {
    return NOT_IMPLEMENTED;
}

// GetPrototype<Elem> === never
// Constructor
const result = map(new Set<number>(), elem => elem + 2) 

It is feasible to infer the instance of a constructor but not the other way around when dealing with just an instance.

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

How to access a variable from outside a promise block in Angular

Is there a way to access a variable declared outside of a promise block within an Angular 6 component? Consider the following scenario: items: string[] = []; ngOnInit() { const url='SOME URL'; const promise = this.apiService.post(url ...

What is the process for adding annotations to a React functional component that includes both props and states?

I'm currently in the process of developing a React Native app that consists of a parent component and a child component. The child component contains a button with an event handler that is located in the parent component. Upon triggering the event han ...

Getting the Value of an Object in an Array in My Angular Project

Within my Angular Application, I am receiving an array of objects from an API: "register":[ { "club": 8, "players": 100, "officials": 10 }, { "male": 7, "female": 2, "other": 1 }, { "Brazil": 5, "France": 1, "Italy": 2, "England": 2 } ] I want to specifi ...

Change the boolean value of a checkbox to text before including it in a GET request to send to an API

Currently, I am working on a school project that involves creating a recipe search application using Angular for the frontend and Laravel for the backend. The application fetches recipes from the Edamam API. I am looking to implement a feature where users ...

Guidelines on declining a pledge in NativeScript with Angular 2

I have encountered an issue with my code in Angular 2. It works fine in that framework, but when I tried using it in a Nativescript project, it failed to work properly. The problem arises when I attempt to reject a promise like this: login(credentials:Cr ...

Struggling to track down the issue in my ts-node express project (Breakpoint being ignored due to generated code not being located)

For my current project, I decided to use the express-typescript-starter. However, when I attempted to debug using breakpoints in VS Code, I encountered an issue where it displayed a message saying "Breakpoint ignored because generated code not found (sourc ...

What could be causing the observable collection to display the correct number of objects, yet have them all appear empty?

I am offering the following service @Injectable() export class LMSVideoResulful { getVideos( enrolmentId : number ) :Observable<Array<Video>> { var x = new Array<Video>(); //https://www.youtube.com/embed/MV0vLcY65 ...

`Incorporating ordered indices within two ngFor loops?`

Below is a demo code where I am explaining my goal through comments: product.component.html <div *ngFor="let item of items1"> <div *ngFor="let item of items2"> <input type="text" value="{{data[getNumber()]}}"> //I aim to fe ...

employing a variable within a function that is nested within another function

I'm encountering an issue where I am using a variable within a nested function. After assigning a value to it, I pass it to the parent function. However, when I call the function, nothing is displayed. function checkUserExists(identifier) { let user ...

Resolve cyclic dependency caused by utilizing the useFactory parameter

I am working with an injectable service that utilizes the useFactory attribute to determine whether it should be injected or if an implemented type should be used instead. import { Injectable } from '@angular/core'; import { Router } from ' ...

The Art of Typing in TypeScript Classes

I am working with an interface or abstract class in TypeScript, and I have numerous classes that implement or extend this interface/class. My goal is to create an array containing the constructors of all these subclasses, while still ensuring that the arra ...

Utilizing the shared styling feature in a NativeScript app by incorporating it through the angular.json stylePre

Trying to implement Angular 6's stylePreprocessorOptions to easily import shared styling into a component using @import 'app'. My NativeScript project is part of a NxWorkspace setup, with its own angular.json file. Following the instructio ...

Deactivating upcoming weeks according to the year in Angular 8

In the user interface, there are dropdowns for selecting a year and a week. Once a year is selected, the list of weeks for that year is displayed in the week dropdown. Now, the requirement is to disable the selection of future weeks. For example, for the ...

Utilize Typescript Functions to Interact with GeoJSON Data in Palantir Foundry

Working on a map application within Palantir utilizing the workshop module. My goal is to have transport routes showcased along roads depending on user inputs. I am familiar with displaying a route using GeoJSON data, but I'm wondering if there is a w ...

Trigger event when ngModel changes

Currently, I am trying to perform a test on a select element... <select [ngModel]="selectedRouters" name="routerName" class="form-control" id="routersSelect" size="12" (ngModelChange)="selectRouters($event)" multiple> <option [value]="route ...

How can I instruct Typescript to exclusively utilize relative paths, with the exception of node_modules?

Is there a method to strictly enforce Typescript to exclusively utilize relative imports for files within the project scope? This is the only query at hand... ...

Encountering difficulties importing an NPM library into StackBlitz

Hey there, I'm currently attempting to replicate an Angular example online but am encountering issues importing my Tabulator Library in stackblitz. I keep receiving an error when trying to import it in the hello component. import Tabulator from &apo ...

Retrieving JSON information using dynamic routes in Next.js

I am working with a json file that contains content for various pages categorized under "service". In my nextJS project, I utilize dynamic routes with a file named "[serviceId].tsx" for routing. This setup is functioning correctly. However, I am facing an ...

Cease the inclusion of the definition file import in the output of TS

I am facing an issue with a third-party library that needs to be dynamically loaded with an authentication key. Since the API is complex, I require type definitions in my TypeScript code for better clarity. In my .tsconfig file, I have set "target": "esn ...

Generating PDF files from HTML using Angular 6

I am trying to export a PDF from an HTML in Angular 6 using the jspdf library. However, I am facing limitations when it comes to styling such as color and background color. Is there any other free library besides jspdf that I can use to achieve this? Feel ...