Can a TypeScript generator function be accurately typed with additional functionality?

Generator functions come with prototype properties that allow for the addition of behavior. The generated generators inherit this behavior. Unfortunately, TypeScript does not seem to recognize this feature, leaving me unsure of how to make it aware.

For instance, let's consider a basic range generator and try to incorporate a map functionality:

/** Generates integers from 0 up to `hi`, incremented by `step`. */
function* range(hi: number, step: number = 1) {
  for (let i = 0; i < hi; i += step) {
    yield i;
  }
}
range.prototype.map = function*<T>(fn: (arg0: number) => T) {
  for (const i of this) {
    yield fn(i);
  }
};

Unfortunately, this doesn't work as expected. Trying something like range(9).map(i => 9 - i) triggers an error stating

Property 'map' does not exist on type 'Generator<number, void, unknown>'.

(The code itself functions properly—it's just the TypeScript typing that falters.)

I find myself daydreaming about a potential syntax to define this behavior manually in case TypeScript fails to infer it automatically. Perhaps something along these lines:

interface of range {
  map<R>(fn: (i: number) => R): Generator<R>;
}

Is there some secret method I have yet to uncover for achieving this? Or is there any plan to integrate such a feature into the language?

Answer №1

When it comes to TypeScript, while it does support adding properties to functions as expandos, there seems to be no built-in way to do the same for generator function prototypes. For classes, you can merge declarations into the instance interface and make modifications to the prototype later. However, this capability is lacking when it comes to generator functions. There is an ongoing issue on microsoft/TypeScript#36086 addressing this concern, but there has not been any progress so far. If you want to advocate for this feature, you can visit the issue, show your support with a thumbs up, and explain why it is important. However, considering the lack of interest thus far, implementation might not happen anytime soon.

If you desire this functionality in TypeScript, you'll have to implement it yourself. Here's one potential method:

function augmentGenerator<T, M, A extends any[]>(gf: (...a: A) => Generator<T>,
    m: M & ThisType<Generator<T> & M>
) {
    Object.assign(gf.prototype, m);
    return gf as (...a: A) => (Generator<T> & M);
}

The concept here is instead of declaring your generator function first and then modifying the prototype afterward, you can use augmentGenerator with a generator function (with type (...a: A) => Generator<T>) and a set of intended prototype changes (of type M). The compiler will then provide a narrowed type as expected ((...a: A) => (Generator<T> & M)).

To provide context when calling augmentGenerator, I am utilizing the ThisType<T> utility type. This helps the compiler recognize that this inside your modifications refers to a suitable generator.

Let's put it into practice:

const range = augmentGenerator(function* (hi: number, step: number = 1) {
    for (let i = 0; i < hi; i += step) {
        yield i;
    }
}, {
    map: function*<T>(fn: (arg0: number) => T) {
        for (const i of this) {
            yield fn(i);
        }
    }
});

The code compiles without issues; upon inspecting the type of range(), we get:

/* const range: (hi: number, step?: number | undefined) =>
   Generator<number> & {
     map: <T>(fn: (arg0: number) => T) => Generator<T>;
   } */

That aligns with your expectations. Consequently, the following works seamlessly at both runtime and compile-time:

for (const i of range(9).map(i => 9 - i)) {
    console.log(i.toFixed(2)); // 9.00, 8.00, etc
}

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

Encountering a problem in React.js and Typescript involving the spread operator that is causing an error

Could someone assist me with my current predicament? I attempted to work with TypeScript and utilize the useReducer hook. const initialState = { a: "a" }; const [state, dispatch] = useReducer(reducer, {...initialState}); I keep encountering an error ...

Unable to resolve all parameters for the RouterUtilities class

My goal is to develop a RouterUtilities class that extends Angular's Router. Despite the app running and compiling smoothly, when I run ng build --prod, it throws an error message like this: ERROR in : Can't resolve all parameters for RouterUtil ...

Accessing Child Properties in Parent Component using Typescript

New to the world of Typescript! Imagine having a component called TitleSubtitle that consists of both a Title and a Subtitle component. The Title component comes with props: interface TitleProps { text: string; } The Subtitle component also has props ...

The Angular2 project using ag-grid-enterprise is currently experiencing difficulties with implementing the License Key

I have a valid license for the ag-grid-enterprise version, but I'm struggling with how to integrate it into my Angular2 project. I've attempted placing the license in the main.ts file using LicenseManager and specifying the enterprise version in ...

Bring in Event Types from React using TypeScript

Is there a way to import Event Types from React and use them in Material-ui? Specifically, I am looking for guidance on how to import KeyboardEvent so that it can be utilized for onKeyDown callback type annotation. I have examined the .d.ts file of Mater ...

Alter text within a string situated between two distinct characters

I have the following sentence with embedded links that I want to format: text = "Lorem ipsum dolor sit amet, [Link 1|www.example1.com] sadipscing elitr, sed diam nonumy [Link 2|www.example2.com] tempor invidunt ut labore et [Link 3|www.example3.com] m ...

What is the best way to transfer information from the window method to the data function in a Vue.js application?

Is there a way to transfer information from the window method to the data function in a vuejs component? Take a look at my window method: window.authenticate = function(pid, receiptKey) { console.log("Authentication"); console.log(this) localStorag ...

Who is responsible for the addition of this wrapper to my code?

Issue with Sourcemaps in Angular 2 TypeScript App Currently, I am working on an Angular 2 app using TypeScript, and deploying it with the help of SystemJS and Gulp. The problem arises when I try to incorporate sourcemaps. When I use inline sourcemaps, eve ...

Exploring Several Images and Videos in Angular

I'm experiencing a challenge with displaying multiple images and videos in my Angular application. To differentiate between the two types of files, I use the "format" variable. Check out Stackblitz export class AppComponent { urls; format; on ...

Error in Angular 5: Google Maps not defined

Having trouble implementing Google Maps on my Angular 5 app. Upon loading the view, I am encountering this error in the JavaScript console: LoginComponent_Host.ngfactory.js? [sm]:1 ERROR ReferenceError: google is not defined at LoginComponent.ngAfterVie ...

What is the best way to transmit a JSON object to a .NET server utilizing SignalR?

I am currently in the process of developing an Angular application that requires sending data from Angular forms to an external server using a .NET Core server and SignalR. While I have successfully established a connection between the Angular client and c ...

Troubleshooting: Issues with accessing Angular/Typescript Class Getter property

I have a class defined as follows: export class Story { id: number; title: string; storyText: string; utcDate: string; get displayDate(): string { const today = new Date(); const postDate = new Date(this.utcDate); ...

The Art of Dynamic Component Manipulation in Angular

The current official documentation only demonstrates how to dynamically alter components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader My goal is to have 3 components: header, section, and footer with the following s ...

Having an issue with displaying the country name and country code in a table using the Angular7 custom pipe

country code: "ab", "aa", "fr", ... I need to create a custom pipe that will convert a countryCode into a countryName, such as: "ab" → "Abkhazian", "ch" → "Chinese", "fr" ...

The issue with the React Hook for window resize not updating remains unresolved

I have a React Hook designed to update the window size on resize, but it's not functioning correctly. Can someone please help explain why this is happening and provide guidance on how to utilize this hook in another component to create a Boolean value ...

What is causing the issue with using transition(myComponent) in this React 18 application?

Recently, I have been immersed in developing a Single Page Application using the latest version of React 18 and integrating it with The Movie Database (TMDB) API. My current focus is on enhancing user experience by incorporating smooth transitions between ...

Searching in Vue based on the selected option is only possible by the selected criteria and not by id, regardless of the

#1 Even if chosen, cannot search by id. The 'name' condition in the loop works well but I am unable to correctly search by id (returns nothing). #2 When selecting an option from the dropdown menu, it only displays until I start typing. I would l ...

Could someone provide a detailed explanation of exhaustMap in the context of Angular using rxjs?

import { HttpHandler, HttpInterceptor, HttpParams, HttpRequest, } from '@angular/common/http'; import { Injectable } from '@core/services/auth.service'; import { exhaustMap, take } from 'rxjs/operators'; import { Authe ...

Next.js API routes encountering 404 error

I am encountering an issue with calling my route API (404) in my new nextjs project. The route API can be found at src/app/api/speed.js Within my page src/app/page.tsx, I have the following code snippet: fetch("api/speed").then(res=>res.json ...

Retrieve the initial token from a union, referred to as an "or list," in Typescript

Is there a way to define a generic type F with the following behavior: type X = F<'a'|'b'|'c'> should result in X being 'a'. And if type X = F<'alpha'|'beta'|'gamma'|'del ...