How to resolve typescript error "Argument of type 'X' is not assignable to parameter of type 'Y'
Asked Answered
P

1

11

UPDATE: Minimum example code reproduction

I have auto-generated code (Graphql-Codegen) that generates the following types. I've condensed it for simplicity.

export type AccessControlList = {
  __typename?: 'AccessControlList';
  id: Scalars['Int'];
  subscriber: Scalars['Int'];
  subscriberRel: MasterSubscriberData;
};

export type Query = {
  __typename?: 'Query';
  workOrders: Array<WorkOrdersGroupBy>;
  viewer: AccessControlAccounts;
};

Since I am only interested in the viewer for this example, I select it by using Typescript Pick

export type ViewerType = Pick<Query, 'viewer'>;

It also auto-generates specialized types for queries that need some, but not all of the possible fields in AccessControlAccounts, and what fields I need vary. I omit the type definition of MasterSubscriberData here but it can be inferred below (but not really needed for this example)

Different queries need to access different portions of the Query structure and thus there are different types generated for each different query, e.g.

export type LeadSourcesQuery = 
{ __typename?: 'Query', 
  viewer: { __typename?: 'AccessControlAccounts', 
            AccessControlList: Array<{ __typename?: 'AccessControlList', 
                                     subscriberRel: { __typename?: 'MasterSubscriberData', 
                                     LeadSources: Array<{ __typename?: 
                                     'LeadSources', id: number> } 
                                    }> 
         } 
};

I'm trying to create a generic function that can return a reference to deeply nested objects commonly accessed, regardless of the subquery types passed (e.g. LeadSourcesQuery, etc). What I came up with works at runtime but fails Typescript static checking.

// expected subscriberRel return type
type SubRetType<T extends ViewerType> =
    T["viewer"]["AccessControlList"][number]["subscriberRel"];

// returning a reference to subscriberRel to avoid doing this nesting everywhere
export const subscriber = <T extends ViewerType>(
    result: T | undefined
): SubRetType<T> | undefined => {
    if (!result) return undefined;
    return result.viewer?.AccessControlList.at(0)?.subscriberRel;
};

Whenever I call this code in src I get the following Typescript error I understand the is related to type compatibility.

Argument of type 'ListCustomerTypesQuery | undefined' is not assignable to parameter of type 'ViewerType | undefined'.ts(2345) Types of property 'viewer' are incompatible. ...is missing the following properties from type AccessControlAccounts

I'm just not sure how to help Typescript understand that whatever T I pass it will always be a sub-type of Query. However, by not using extends on the super-type Query, Typescript doesn't understand that the nested fields of AccessControlList.subscriberRel exist at "compile time". And the specialized generic type T doesn't really extend the super type Query.

Whenever I'm not using generics, I can use, say Extract<Query, ListCustomerTypesQuery> to extract common properties but that doesn't make the error go away when I'm using generics Extract<Query, T>.

I am able to get around this error by casting it as so, but I don't want to do this for ever single function call.

subscriber(result.value as ViewerType);

I would prefer to clean it up and keep it simply as

subscriber(result.value);
Plainspoken answered 5/5, 2022 at 13:35 Comment(11)
Do you have a minimal reproducible example that you could share with us in a playground?Pliocene
replit.com/@aam88/Compatibilty-error#index.ts @catgirlkellyPlainspoken
Preferably in a playground instead if possible? Repl.it is not very supportive in the editor of TypeScript.Pliocene
@catgirlkelly, can you provide any preferred recommendations for a playground? I was trying to use one that allow me split up code in different files to reproduce the error since I don't see the same generic ts error when it is all lumped together in a single file, in say, codepenPlainspoken
The official one @ typescriptlang.org/playPliocene
@catgirlkelly, added new link in the UPDATE section of the descriptionPlainspoken
Alright thanks I will take a look when I've got more time.Pliocene
Hmm, sorry I don't know a good way to solve this one... Maybe in a day I will get a spark of inspiration.Pliocene
@catgirlkelly no worriesPlainspoken
Wouldn't a deep partial type be the solution here? It would make it a subtype of Query?Pliocene
Does this work for you?Pliocene
P
2

Any subtype of Query sounds like a task for DeepPartial. You want any possible subtype of Query which means all of its properties are optional. Here's DeepPartial:

type DeepPartial<T> = T extends object ? {
    [P in keyof T]?: DeepPartial<T[P]>;
} : T;

You might need to make it work for arrays as well.

Then we change the signature of subscriber to take a deep partial of ViewerType:

export const subscriber = <T extends ViewerType>(
  result: DeepPartial<T> | undefined /** Extract<ViewerType,T> doesn't work either */
): SubRetType<T> | undefined => {

Unfortunately you still need to cast the last return, as TypeScript doesn't understand that you want only subtypes:

return result.viewer?.AccessControlList?.at(0)?.subscriberRel as SubRetType<T>;

Playground

Pliocene answered 19/5, 2022 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.