Specifying data types for keys within a generic parameter that is inferred

Objective

I am currently developing a utility with the following interface:

interface F<M extends Record<string, Function>> {
  (
    listeners: M, 
    options: { filter: e => boolean }
  ): void
}

An important aspect here is the filter option, which will receive events from all provided listeners.

My goal is to make the type of parameter e dependent on the provided listeners:

f({
  e1: (e: E1) => {}
}, {
  filter: e => {} // <= `e` should be of type `E1`
})

f({
  e1: (e: E1) => {},
  e2: (e: E2) => {}
}, {
  filter: e => {} // <= `e` should be of type `E1 | E2`
})

Solution Approach

I have discovered a way to achieve this:

interface F<T> {
  <E extends keyof T>(
    listeners: Pick<T, E>, 
    options: { filter: (e: Arg<T[E]>) => boolean }
  ): void
}

This solution effectively sets the correct parameter for the filter function and enforces constraints on the listeners, preventing the addition of keys not defined in T.

Challenges Faced

The only minor issue I encountered is that TypeScript does not provide available key hints for the listeners object.

For example, if I declare

let f: F<{ e1: Function, e2: Function }>
and use f({ ... }) with ctrl+space – it displays numerous unrelated suggestions instead of just e1 and e2.

I understand that this behavior is expected with the current implementation. TypeScript cannot ascertain available options as I define the listeners type, and TS can only validate it against the constraint of keyof T.

Is there a possibility to enable typehints in this scenario without compromising the proper inference of the filter callback parameter or the validation of keys for the listeners parameter?

I experimented with various options in the sandbox, but none fully meet all my requirements.

Answer №1

You have limited control over how TypeScript reports errors or provides IntelliSense suggestions. A feature request has been open for custom error messages at microsoft/TypeScript#23689, but no such request exists for custom IntelliSense suggestions, and currently neither feature is available. You may need to find an approach that suits your most critical use cases while compromising on less important ones.

One potential unexplored approach is:

interface F<T> {
    <M extends T & Record<Exclude<keyof M, keyof T>, never>>(
        map: M, opts: OptsFromMap<M>
    ): void
}

Constraining M to T alone won't prevent excess properties because types can be extended with additional properties. To mimic something like excess property checking for generics, the generic must be constrained so that excess properties are set to the impossible never type. This ensures errors will occur if an excess property is added. While the error message may not explicitly state "excess properties are not allowed," it will point out that the property doesn't match the expected type.

To test this approach:

declare let f: F<EventsMap>
f({ e1: e => e, }, { option: x => x })
f({ e1: e => e, e2: e => e, }, { option: x => x })
f({ e1: e => e, foo: e => e, }, { option: x => x }); // error!
//              ~~~  ~

The results are promising, with accurate IntelliSense, correct inferred types, and errors triggered by excess properties. Although the wording of the error message may not be ideal, it represents the best achievable user experience for generic extensions.

(Link to 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 ng-repeat directive disables input controls within the tfoot

When working with JSON data, I encountered a situation where I needed to display different types of student details in a table. For one specific type of student, namely partners, I wanted to include input controls such as checkboxes and buttons. However, e ...

Utilize mapGetter and mapMutations in Vuex with TypeScript without the need for class-style components syntax

After completing a project in Vue, I found myself getting a bit confused without static types. To address this, I decided to incorporate TypeScript into my project while still maintaining the traditional way of writing code, without classes and decorators. ...

Error in Docker: Unable to resolve due to sender error: context has been terminated

After attempting to build my docker image for the project in VS Code terminal, I ran into an error. What are some possible reasons for this issue? Along with this question, I have also shared a screenshot of the error logs. ERROR: failed to solve: error ...

Update the router URL without switching pages, yet still record it in the browser history

One of the features on my search page allows users to perform searches and view results. Initially, I faced a challenge in updating the router URL without navigating, but I managed to overcome this by utilizing the "Location" feature. In my ngOnInit meth ...

Encountering an issue in a Next.js application while building it, where an error is triggered because the property 'protocol' of 'window.location' cannot be destructured due to being undefined

While building my nextjs application, I encountered the following error. My setup uses typescript for building purposes, although I am only using JavaScript. Build error occurred: TypeError: Cannot destructure property 'protocol' of 'window ...

How to open a print preview in a new tab using Angular 4

Currently, I am attempting to implement print functionality in Angular 4. My goal is to have the print preview automatically open in a new tab along with the print popup window. I'm struggling to find a way to pass data from the parent window to the c ...

The impact of placing a method in the constructor versus outside it within a class (yielding identical outcomes)

These two code snippets appear to produce the same result. However, I am curious about the differences between the two approaches and when it would be more appropriate to use one over the other. Can someone provide insight into this matter? Snippet 1: c ...

Exploring Typescript - Managing generic data types while preserving runtime type details

My situation involves a generic container type that can hold various types of data. The data's type is determined at runtime by a "type" field that contains a string: type Type = "Numeric" | "Text"; type DataTypeFor<T extends Type> = T ext ...

Using Pydantic to define models with both fixed and additional fields based on a Dict[str, OtherModel], mirroring the TypeScript [key: string] approach

Referencing a similar question, the objective is to construct a TypeScript interface that resembles the following: interface ExpandedModel { fixed: number; [key: string]: OtherModel; } However, it is necessary to validate the OtherModel, so using the ...

The onClick function was not recognized as a valid function when it was called

I encountered an error when passing an onClick function as a prop in my React app from one component to another. The error message displayed is Uncaught TypeError: handleOnClick is not a function. Here is the function I am passing: propList = ['a&apos ...

Troubleshooting: Angular 2 failing to load test component

Despite successfully getting my app.component to work in Angular 2, I am facing issues with a test one. It seems to be stuck at the "Loading..." message in my HTML. Can someone please take a look at my code and point out where I might have gone wrong? I fo ...

Angular's counterpart to IWebProxy

When using C#, I am able to: public static IWebProxy GetWebProxy() { var proxyUrl = Environment.GetEnvironmentVariable("HTTPS_PROXY"); if (!string.IsNullOrEmpty(proxyUrl)) { var proxy = new WebProxy { Address = new Ur ...

Is it possible to create a single model with different variations, each with specific required fields?

If you're working with our API, you'll likely need to create an Order. While the order model remains consistent across different endpoints, the required fields may vary: For a POST request, only a few fields are required. With a PATCH request, ...

Exploring TypeScript and node.js development using Visual Studio 2012 express

Is there a way to successfully write, build, and execute a node.js application in Visual Studio? I have already installed the TypeScript extension on VS as well as the node.js package. However, when I attempt to create a new project of the TypeScript type, ...

Can a JavaScript object be created in TypeScript?

Looking for a way to utilize an existing JavaScript "class" within an Angular2 component written in TypeScript? The class is currently defined as follows: function Person(name, age) { this.name = name; this.age = age; } Despite the fact that Java ...

Develop an rxjs pipeline that merges values according to their type prior to executing them in an async manner using concatMap

In my code, there's an eventStream that deals with different types of events and sends them to the server via HTTP. import { from, Observable } from 'rxjs'; import { concatMap } from 'rxjs/operators'; type Update = number[]; inte ...

Encountering an issue with Angular 12 where a TypeError is being thrown, specifically stating "Cannot read properties of null (reading 'length') at

I encountered an error message while making a http request in my Angular Service. Strangely, this error only occurs after I logout, but it disappears upon logging back in: Below is the code snippet of my authentication Service: import { Injectable } from ...

Guide to using Enums in *ngIf statements in Angular 8

I have defined an enum type in my TypeScript file, and I want to use it as a condition in my HTML code. However, when trying to access the "values" of the enum, they appear to be undefined even though I have declared them and inherited from the exported en ...

Arrange chat participants based on the latest messages using React

Looking for a way to efficiently organize the array of users in my ReactJS chat application according to the latest and most recent messages they have sent, constantly updating with each new message, similar to WhatsApp or Telegram. Below are simplified s ...

Transmitting FormControl to External FormBuilder in Angular 8

I am currently working on creating a wrapper for Angular Material Select. I am trying to find out how to transfer the FormControl from the Inner Component (Material dropdown select) to an external Parent Component Formbuilder. I am exploring different synt ...