Refining Typescript type with specific error for generics

Seeking assistance to comprehend the situation: TS playground

The situation involves a store with an exec method, where narrowing down the type of exec param is crucial for a sub process. However, an error seems to arise due to the store type being generic.

type Param<Options> = {
  [K in keyof Options]: Readonly<{
    id: K,
    options: Options[K],
  }>
}[keyof Options];

interface Store<Options> {
    exec: (nextState: Param<Options>) => void
}

type ParentOptions = {
    'a': { a: string },
} & SubOptions

type SubOptions = {
    'b': { b: number },
}

function test(
    parentFlowExec: (nextState: Param<ParentOptions>) => void,
    subFlowExec: (nextState: Param<SubOptions>) => void,
    
    parentNonGeneric: { exec: (nextState: Param<ParentOptions>) => void },
    subNonGeneric: { exec: (nextState: Param<SubOptions>) => void },
    
    parentFlow: Store<ParentOptions>,
    subFlow: Store<SubOptions>,
    
) {
    parentFlowExec = subFlowExec; // error: ok
    subFlowExec = parentFlowExec; // passed

    parentNonGeneric = subNonGeneric; // error: ok
    subNonGeneric = parentNonGeneric; // passed

    parentFlow = subFlow; // error: ok
    subFlow = parentFlow; // error ??

    // Intended usage scenario
    subProcess(parentFlow);
}

function subProcess(flowStore: Store<SubOptions>) {
    flowStore.exec({ id: 'a', options: { a: 'a' } }); // can't call with 'a'
    flowStore.exec({ id: 'b', options: { b: 3 } }); // ok
}

Update: Moved the Param and have it working but still don't understand why nested them doesn't work

interface Store<Options> {
    exec: (nextState: Options) => void
}
// parent2: Store2<Param<ParentOptions>>,
// sub2: Store2<Param<SubOptions>>,

Answer №1

To provide an explanation for your query, let's start by revisiting the concept of "variance." In this context, I will be referring to definitions outlined in Microsoft's .NET documentation (with the exception of bivariance, which is not documented). Here is a summary of the different types of variance and their implications:

Variance Definition Allowed Substitutions
Bivariance Covariance and Contravariance simultaneously Supertype -> Subtype, Subtype -> Supertype
Covariance Allows usage of a more derived type than specified Supertype -> Subtype
Contravariance Allows usage of a less derived type than specified Subtype -> Supertype
Invariance Restricted to the originally specified type none

Now, let's identify whether your types are considered as supertypes or subtypes:

type T1 = SubOptions extends ParentOptions ? true : false; // false
type T2 = ParentOptions extends SubOptions ? true : false; // true

Based on the above, we conclude that ParentOptions is a subtype of SubOptions, with the latter being its supertype. This scenario signifies attempting to assign a subtype when a supertype is anticipated when assigning parentFlow labeled as Store<ParentOptions> to subFlow designated as Store<SubOptions>.

Referring back to the variance table, this situation aligns with the requirement for covariance. However, encountering an error implies involvement of either contravariance or invariance. Upon analyzing the assignment of subFlow to

parentFlow</code, where a <em>supertype expectation meets a subtype provision</em>, the conclusion reveals the application of <em>invariant</em> behavior. The assertion made by <a href="https://stackoverflow.com/users/8495254/captain-yossarian">@captain-yossarian</a> in this <a href="https://stackoverflow.com/questions/67482793/typescript-narrowing-type-with-generic-error#comment119279210_67482793">comment</a> proves valid:</p>
<blockquote>
<p>I believe that it is because subFlow and parentFlow are invariant to each other.</p>
</blockquote>
<p>Nonetheless, this enforced design limitation highlighted in TypeScript (as referenced in Anders Hejlsberg's <a href="https://github.com/microsoft/TypeScript/issues/32674#issuecomment-716178615" rel="nofollow noreferrer">comment</a> addressing a related concern) sacrifices flexibility for reliability. By eliminating the <code>[keyof Options]
indexing component, the contravariant assignment becomes feasible.

Regarding the proposed resolution strategy, restructuring the positioning of Params outward results in the conversion of parameter types to covariant attributes due to the absence of aliasing within

T[keyof T]</code here. It's important to note that at its core structure, the type <code>Param
essentially represents:
type Param<Options> = Options[keyof Options]
, merely mapped1.

An illustrative example0 of the solution process can be observed as follows:

type Param<Options> = {
  [K in keyof Options]: Readonly<{
    id: K,
    options: Options[K],
  }>
}[keyof Options];

interface Store<Options> {
    exec: (nextState: Options) => void
}

type SuperOptions = { 'b': { b: number } }
type SubOptions = { 'a': { a: string } } & SuperOptions

const test1 = (subtype: Store<Param<SubOptions>>) => subProcess1(subtype); // OK, Subtype -> Supertype, covariance
const test2 = (supertype: Store<Param<SuperOptions>>) => subProcess2(supertype); // error, Supertype -> Subtype, contravariance

const subProcess1 = (supertype: Store<Param<SuperOptions>>) => supertype.exec({ id: 'b', options: { b: 3 } }); // ok
const subProcess2 = (subtype: Store<Param<SubOptions>>) => subtype.exec({ id: 'b', options: { b: 3 } }); // ok

Playground


0 Your naming convention adds a layer of complexity to an already intricate issue: designating a subtype as

ParentOptions</code and a supertype as <code>SubOptions</code, despite representing the reverse relationship. To enhance clarity, I have renamed them <code>SubOptions
and SuperOptions accordingly.

1 As discussed in the comments, the relationship between

Store<Param<SubOptions>>
and
Store<Param<SuperOptions>></code portrays a <em>covariant</em> nature within the solution approach. However, the essence of <code>T[keyof T]</code in this context demonstrates a <em>contravariant</em> aspect, as elucidated in Anders's <a href="https://github.com/microsoft/TypeScript/issues/32674#issuecomment-716192565" rel="nofollow noreferrer">comment</a>—the <code>SuperOptions
supertype contains fewer properties than the SubOptions subtype without any distinctive features).

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

Highchart in ionic 2 not displaying

I inserted code for a highchart on my webpage, but it's not appearing I followed instructions from this video tutorial https://www.youtube.com/watch?v=FSg8n5_uaWs Can anyone help me troubleshoot this issue? This is the TypeScript code I used: ts; ...

Angular HttpClient Catch Return Value

In my quest to develop a universal service for retrieving settings from the server, I've encountered an issue. When errors arise, I want to intercept them and provide a default value (I have a predetermined configuration that should be utilized when e ...

Tips on creating a Jasmine unit test scenario for a function that triggers onerror of an image tag

I am facing an issue with the code below as the test case is failing //test case it('should display default image if the image link in people finder result does not exist', fakeAsync(() => { debugger component.resultsItem = M ...

React: Using useState and useEffect to dynamically gather a real-time collection of 10 items

When I type a keystroke, I want to retrieve 10 usernames. Currently, I only get a username back if it exactly matches a username in the jsonplaceholder list. For example, if I type "Karia", nothing shows up until I type "Karianne". What I'm looking f ...

Acquiring an element through ViewChild() within Angular

I am in need of a table element that is located within a modal. Below is the HTML code for the modal and my attempt to access the data table, which is utilizing primeng. <ng-template #industryModal> <div class="modal-body"> <h4>{{&a ...

Struggling to locate a declaration file for the 'cloudinary-react' module? Consider running `npm i --save-dev @types/cloudinary-react` or exploring other options to resolve this issue

Currently, I am working with Typescript in React. Strangely, when I try to import the following: import { Image } from 'cloudinary-react'; I encounter this error: Could not find a declaration file for module 'cloudinary-react'. ' ...

Leverage TypeScript for modifying local node package alterations

As a newcomer to npm and TypeScript, I may be overlooking something obvious. Our team has developed a node package in TypeScript for internal use, resulting in the following file structure: src/myModule.ts myModule.ts The contents of myModule.ts are as f ...

ts-jest should replace the character '@' with the '/src' folder in the map configuration

I have set up a node project using TypeScript and configured various paths in my tsconfig.json file, such as: "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl' ...

What is the best way to create two MUI accordions stacked on top of each other to each occupy 50% of the parent container, with only their contents scrolling

I am looking to create a layout with two React MUI Accordions stacked vertically in a div. Each accordion should expand independently, taking up the available space while leaving the other's label untouched. When both are expanded, they should collect ...

Leverage Sinon's fakeServer in combination with promises and mocha for efficient

Having an issue here: I need to test a method that involves uploading data to an AWS S3 bucket. However, I don't want the hassle of actually uploading data each time I run my tests or dealing with credentials in the environment settings. That's w ...

Is there a way to detect and handle errors triggered by a callback function?

My component has the following code snippet: this.loginService.login(this.user, () => { this.router.navigateByUrl('/'); }); Additionally, my service contains this method: login(credentials, callback) { co ...

Tips for managing errors when utilizing pipe and mergemap in Angular

In the code snippet provided, two APIs are being called. If there is an error in the first API call, I want to prevent the second API call from being made. Can you suggest a way to handle errors in this scenario? this.userService.signUp(this.signUpForm.v ...

Failure in retrieving values from AngularFire2 Subscribe

I am encountering an issue with the code in my authService constructor( private afAuth: AngularFireAuth, private db: AngularFireDatabase, private router: Router ) { this.authState = afAuth.authState; this.authState.subscribe((use ...

Implementing Firebase-triggered Push Notifications

Right now, I am working on an app that is built using IONIC 2 and Firebase. In my app, there is a main collection and I am curious to know if it’s doable to send push notifications to all users whenever a new item is added to the Firebase collection. I ...

Enhancing Vue functionality with vue-class-component and Mixins

In my Vue project, I am using vue-class-component along with TypeScript. Within the project, I have a component and a Mixin set up as follows: // MyComp.vue import Component, { mixins } from 'vue-class-component' import MyMixin from './mixi ...

Are you harnessing the power of Ant Design's carousel next and previous pane methods with Typescript?

Currently, I have integrated Ant Design into my application as the design framework. One of the components it offers is the Carousel, which provides two methods for switching panes within the carousel. If you are interested in utilizing this feature using ...

An error occurs when trying to use AWS.Comprehend as a constructor in the aws JavaScript SDK

I'm attempting to utilize the Amazon Comprehend API using the AWS JavaScript SDK. However, I keep encountering Uncaught (in promise): TypeError: undefined is not a constructor (evaluating 'new AWS.Comprehend... ' What am I doing incorr ...

Angular 4: Transform a string into an array containing multiple objects

Recently, I received an API response that looks like this: { "status": "success", "code": 0, "message": "version list", "payload" : "[{\"code\":\"AB\",\"short\":\"AB\",\"name\":\"Alberta&b ...

Unable to find a solution to Angular response options

I'm having trouble saving an item to local storage when receiving a 200 response from the backend. It seems like the request status is not being recognized properly. options = { headers: new HttpHeaders({ 'Content-Type': &apos ...

Ways to effectively implement a function type specified in an interface

Consider the following interface: interface MyProps { onEvent: (name: string, data: any) => void; } Is there a way to utilize the function type in order to avoid unused parameter errors during compilation? eventHandler = (name: string, data: any) = ...