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

Acquire Superheroes in Journey of Champions from a REST endpoint using Angular 2

Upon completing the Angular 2 Tour of heroes tutorial, I found myself pondering how to "retrieve the heroes" using a REST API. If my API is hosted at http://localhost:7000/heroes and returns a JSON list of "mock-heroes", what steps must I take to ensure a ...

I am continuously receiving the message "Alert: It is important for every child in a list to possess a distinct "key" prop." while working with the <option> list

Having trouble generating unique keys for my elements. I've tried different values and even Math.random() but still can't seem to get it right. I know the key should also be added to the outer element, but in this case, I'm not sure which on ...

Exploring how to utilize class properties within Angular templates

I am facing an issue with using a template property in Angular. Despite my attempts, it is not functioning as expected and I am unable to pinpoint the cause of the problem. To illustrate, I have set up a demonstration here: https://github.com/Fulkerson/an ...

Convert parameterized lambdas for success and failure into an observable using RxJS

There is a function exported by a library that I am currently using: export function read( urlOrRequest: any, success?: (data: any, response: any) => void, error?: (error: Object) => void, handler?: Handler, httpClient?: Object, ...

Utilizing an AwsCustomResource in AWS CDK to access JSON values from a parameter store

I found a solution on Stack Overflow to access configurations stored in an AWS parameter. The implementation involves using the code snippet below: export class SSMParameterReader extends AwsCustomResource { constructor(scope: Construct, name: string, pr ...

Combining two objects/interfaces in a deep merging process, where they do not intersect, can result in a final output that does not

When attempting to merge two objects/interfaces that inherit from the same Base interface, and then use the result in a generic parameter constrained by Base, I encounter some challenges. // please be patient type ComplexDeepMerge<T, U> = { [K in ( ...

I lost my hovering tooltip due to truncating the string, how can I bring it back in Angular?

When using an angular ngx-datatable-column, I added a hovering tooltip on mouseover. However, I noticed that the strings were too long and needed to be truncated accordingly: <span>{{ (value.length>15)? (value | slice:0:15)+'..':(value) ...

Enhancing a Given Interface with TypeScript Generics

I am looking to implement generics in an Angular service so that users can input an array of any interface/class/type they desire, with the stipulation that the type must extend an interface provided by the service. It may sound complex, but here's a ...

Tips on resolving issues with cellclickable functionality in Angular with gridster2

VERSION: ^9.3.3 HTML <button (click)="toggleEditing()">{ editing ? 'cancel' : 'editing' }</button> <button>ADD</button> <gridster [options]="options"> &l ...

Avoid risky assigning value of type `any`

Currently, I am incorporating TypeScript into my client-side application. However, upon running the application, I encounter two specific errors: @typescript-eslint/no-unsafe-assignment: Unsafe assignment of an `any` value. @typescript-eslint/no-unsafe-me ...

What is the best way to create a generic variable and function that utilize the same type?

I am looking for a structure similar to interface propType { value: T action: (value: T) => void } The variable T can be any type, but it must be consistent for both value and action. Using any is not suitable as it could lead to a mismatch in ty ...

Is it necessary to list all potential strings for accessibilityRole?

When working with accessibilityRole in React Native, I am wondering if there is a way to import all the possible strings instead of typing them out manually. createAccessibilityRole(parent: Element): string { if(isLink) return 'link' return ...

Can a custom subscribe() method be implemented for Angular 2's http service?

Trying my hand at angular2, I discovered the necessity of using the subscribe() method to fetch the results from a get or post method: this.http.post(path, item).subscribe( (response: Response)=> {console.log(response)}, (error: any)=>{console.l ...

The Angular route successfully navigated to the page, but the HTML content was not

Whenever I select the Home option in the navigation bar, it takes me to the home URL but doesn't display the HTML content. Below is my app.routing.module.ts code: import { Component, NgModule } from '@angular/core'; import { RouterModule, Ro ...

Using the increment operator within a for loop in JavaScript

this code snippet causes an endless loop for (let i = 0; ++i;) { console.log(i) } the one that follows doesn't even run, why is that? for (let i = 0; i++;) { console.log(i) } I want a thorough understanding of this concept ...

Describing a function in Typescript that takes an array of functions as input, and outputs an array containing the return types of each function

Can the code snippet below be accurately typed? function determineElementTypes(...array: Array<(() => string) | (() => number) | (() => {prop: string}) | (() => number[])>) { /// .. do something /// .. and then return an array ...

What is the method by which the Material-UI Button component determines the properties for the component that is passed to the `component` prop

Could someone please clarify how Material-UI enhances the properties of its Button component by incorporating the properties of a specific component if passed in the component attribute? interface MyLinkProps extends ButtonBaseProps { someRandomProp: str ...

Angular 5 error: Decorators do not support function expressions

I am struggling to compile my angular project using the command ng build --prod The issue arises in the IndexComponent : index.componenent.ts import { Component, OnInit } from '@angular/core'; import { indexTransition } from './index.anim ...

Error message indicating a problem with global typings in Angular 2 when using Webpack is displayed

My project is utilizing angular 2 with webpack and during the setup process, I encountered Duplicate identifier errors when running the webpack watcher: ERROR in [default] /angular/typings/globals/node/index.d.ts:370:8 Duplicate identifier 'unescape& ...

Assign variable data to properties within an immutable object within a React component

I have declared a Const in my config.service.ts file like this: export const mysettings={ userid:"12324", conf:{ sessionDuration:30, mac:"LON124" } } I am using this constant in various components. However, instead of hardcoding these val ...