Concern with generic characteristics during type conversion

One of the utilities in my library is a type similar to the following:

type Action<Model extends object> = (data: State<Model>) => State<Model>;

This utility type allows the declaration of an "action" function that operates against a generic Model.

The argument data in the "action" function is typed using another utility type exported:

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

The State utility type takes the input Model and creates a new type by removing all properties of type Action.

For example, here is a basic user implementation:

interface MyModel {
  counter: number;
  increment: Action<Model>;
}

const myModel = {
  counter: 0,
  increment: (data) => {
    data.counter; // Typed as `number`
    data.increment; // Does not exist
    return data;
  }
}

However, I am facing an issue when defining a generic model along with a factory function to create instances of the model.

For instance:

interface MyModel<T> {
  value: T;
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Doesn't exist
      data.doSomething; // Exists
      return data;
    }
  };
}

In this example, I expect the data argument to still have the generic value property even after the doSomething action has been removed. However, this is not the case.

I believe this is due to the generic T intersecting with the Action type and getting removed from the argument type.

Is there a workaround for this limitation in TypeScript?

You can access the full code snippet for debugging here: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

Answer №1

Dealing with generic type parameters in conditional types in TypeScript can be quite challenging. The compiler tends to defer the evaluation of extends when it involves a type parameter.

However, there is an interesting workaround using an equality relation instead of an extends relation. TypeScript can handle type equality much better, especially in generic constraints. Here's a simple example to illustrate this:

function m<T, K>() {
  type Bad = T extends T ? "YES" : "NO" // Unresolvable in ts

  type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // Resolvable

  type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO" // Unresolvable
}

This technique of using equality relations can be leveraged to identify specific types with precision. While it may not be suitable for all scenarios, it can provide accurate results in certain cases. Let's take a look at how we can filter types based on a simple function signature like (v: T) => void:

interface Model<T> {
  value: T,
  other: string,
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]: Identical<M[K], (v: T) => void, never, K>
  }
}

By utilizing this method, we can effectively filter out specific types based on a given function signature, ensuring a precise match rather than a loose extends match. This approach may have its limitations, but it demonstrates a unique way to handle type relations in TypeScript.

Answer №2

I wish there was a way to indicate that T is not an instance of Action. It's like the opposite of extending.

Just as you mentioned, the issue lies in the absence of a negative constraint. I am hopeful that such a feature will be implemented soon. In the meantime, I suggest a workaround like this:

type NonActionKeys<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];

// UPDATE: replacing `Omit` with `Pick` in this part of the code.
type State<Model extends object> = Pick<Model, NonActionKeys<Model, Action<any>>>;

type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T;
  doSomething: Action<MyModel<T>>;
}

function createModel<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // It exists now 😉
      data.doSomething; // It doesn't exist 👍
      return data;
    }
  } as MyModel<any>; // <-- The trick!
                     // as the type of `T` is unknown at this point
                     // it can be anything
}

Answer №3

quantity and size always tend to upset the compiler. To resolve this issue, you can try the following approach:

{
  size,
  quantity: 1,
  modify: (data: Partial<Item<T>>) => {
   ...
  }
}

Using the Partial utility type ensures that you are covered even if the modify method is absent.

Check out the Stackblitz example

Answer №4

After reading this multiple times, I'm still not quite grasping the goal you are trying to achieve. It seems like you want to exclude transform from the given type, specifically transform. This can be accomplished easily by utilizing the Omit utility:

interface Thing<T> {
  value: T; 
  count: number;
  transform: (data: Omit<Thing<T>, 'transform'>) => void; // specifying the argument type as Thing without transform
}

// Factory function that accepts a generic
function makeThing<T>(value: T): Thing<T> {
  return {
    value,
    count: 1,
      transform: data => {
        data.count; // exists
        data.value; // exists
    },
  };
}

It's hard to say if this is exactly what you were looking for due to the complexity introduced by the additional utility types. Hopefully, this information proves helpful.

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

While running tslint in an angular unit test, an error was encountered stating 'unused expression, expected an assignment or function call'

Is there a method to resolve this issue without needing to insert an ignore directive in the file? Error encountered during command execution: ./node_modules/tslint/bin/tslint -p src/tsconfig.json --type-check src/app/app.component.spec.ts [21, 5]: unuse ...

Tips for sending a post request using Angular 4

I'm currently facing an issue while attempting to execute a post request using Angular 4 to transmit lat and lng parameters: let data = {lat: this.newLat, lng: this.newLng}; this.http.post(url, data) .map(response => response.json()) .subscri ...

Disabling a specific tab in an array of tabs using Angular and Typescript

Displayed below are 5 tabs that can be clicked by the user. My goal is to disable tabs 2 and 3, meaning that the tab names will still be visible but users will not be able to click on them. I attempted to set the tabs to active: false in the TypeScript fi ...

Retrieve data from a JSON object within an HTML document

How do I display only the value 100 in the following div? <div> {{uploadProgress | async | json}} </div> The current displayed value is: [ { "filename": "Mailman-Linux.jpg", "progress": 100 } ] Here is my .ts file interface: interface IU ...

Embedding a TypeScript React component within another one

Currently, I'm facing an issue with nesting a TypeScript React component within another one, as it's causing type errors. The problem seems to be that all props need to be added to the parent interface? Is there a way to handle this situation wi ...

What is the most efficient method for line wrapping in the react className attribute while utilizing Tailwind CSS with minimal impact on performance?

Is there a more efficient way to structure the className attribute when utilizing tailwind css? Which of the examples provided would have the least impact on performance? If I were to use an array for the classes and then join them together as shown in e ...

Encountered an unexpected token error while using Jest with TypeScript, expecting a semicolon instead

I have been struggling to configure jest for use with typescript and despite trying several solutions, I am still facing issues. The error SyntaxError: Unexpected token, expected ";" keeps popping up, indicating that the configuration may not be compatible ...

Having difficulty implementing a versatile helper using Typescript in a React application

Setting up a generic for a Text Input helper has been quite challenging for me. I encountered an error when the Helper is used (specifically on the e passed to props.handleChange) <TextInput hiddenLabel={true} name={`${id}-number`} labelText=" ...

Variable type linked to interface content type

Is it possible to link two fields of an interface together? I have the following interface: export interface IContractKpi { type: 'shipmentVolumes' | 'transitTime' | 'invoices'; visible: boolean; content: IKpiContent; } ...

Bringing in External Components/Functions using Webpack Module Federation

Currently, we are experimenting with the react webpack module federation for a proof of concept project. However, we have encountered an error when utilizing tsx files instead of js files as shown in the examples provided by the module federation team. We ...

What is the best way to divide a string into an array containing both linked and non-linked elements?

I'm struggling to find the right solution to my problem. I need to create a view that is enclosed in a clickable div. The content will consist of plain text mixed with clickable URLs - the issue arises when clicking on a link also triggers the method ...

Does the term 'alias' hold a special significance in programming?

Utilizing Angular 2 and Typescript, I have a component with a property defined as follows: alias: string; Attempting to bind this property to an input tag in my template like so: <input class="form-control" type="text" required ...

Retrieving a specific data point from the web address

What is the most efficient way to retrieve values from the window.location.href? For instance, consider this sample URL: http://localhost:3000/brand/1/brandCategory/3. The structure of the route remains consistent, with only the numbers varying based on u ...

Refreshing Angular2 View After Form Submission

Currently, I am in the process of developing a basic CRUD application with Angular2. The application comprises of a table that displays existing records and a form for adding new records. I am seeking guidance on how to update the table to show the new rec ...

Launching Nest.js application from Visual Studio Code

Currently experimenting with a new framework called which seems promising as it integrates TypeScript into Node, similar to Angular. I'm using the starter template from https://github.com/kamilmysliwiec/nest-typescript-starter. I can start it withou ...

Buttons for camera actions are superimposed on top of the preview of the capacitor camera

I am currently using the Capacitor CameraPreview Library to access the camera functions of the device. However, I have encountered a strange issue where the camera buttons overlap with the preview when exporting to an android device. This issue seems to on ...

Show the key and value of a JSON object in a React component

When attempting to parse a JSON data file, I encountered an error message stating: "Element implicitly has an 'any' type because expression of type 'string' can't be used to the index type." The JSON data is sourced locally from a ...

Angular - Implementing validation for maximum character length in ngx-intl-tel-input

Utilizing the ngx-intl-tel-input package in my Angular-12 project multistep has been quite promising. Below is a snippet of the code: Component: import { SearchCountryField, CountryISO, PhoneNumberFormat } from 'ngx-intl-tel-input'; expor ...

combineLatest will trigger only for the initial event

I am looking to combine 3 events and trigger a service call when any of them are fired. Currently, I am using the combineLatest method, but it seems to only work when the first event is triggered by filterChanged. The issue here is that filterChanged is a ...

Tips for effectively passing generics to React Hooks useReducer

I am currently working with React Hooks useReducer in conjunction with Typescript. I am trying to figure out how to pass a type to the Reducer Function using generics. interface ActionTypes { FETCH, } interface TestPayload<T> { list: T[]; } inter ...