A convenient way to encapsulate autogenerated functions in a typesafe wrapper is to simply pass in "__typename" as a parameter to dynamically call them

Utilizing the fully typesafe auto-generated code provided by the amazing graphql-codgen/vue, I have streamlined its usage in my project by creating a small wrapper to simplify common configuration tasks for my Users. This includes activities such as defining cache behavior, automatically updating the cache, and deconstructing results into the appropriate types and formats.

The wrapper functions seamlessly with JS and accepts any type inputs, however, I am keen on enhancing it with type safety. Given that graphql-codegen already generates all methods and types in a typesafe manner, I believe there must be a way to achieve this, possibly using discriminating unions...

To illustrate my question with a code snippet: I possess this auto-generated code:

//File GQLService.ts
export type CustodiansList = (
  { __typename: 'Query' }
  & { custodiansList?: Maybe<Array<(
    { __typename: 'Custodian' }
    & Pick<Custodian, 'id' | 'name' | 'street' | 'zip' | 'city' | 'telephone' | 'createdAt' | 'updatedAt'>
  )>> }
);

type ReactiveFunctionCustodiansList = () => CustodiansListVariables

/**
 * __useCustodiansList__
 *
 * To run a query within a Vue component, call `useCustodiansList` and pass it any options that fit your needs.
 * When your component renders, `useCustodiansList` returns an object from Apollo Client that contains result, loading and error properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
 *
 * @example
 * const { result, loading, error } = useCustodiansList(
 *   {
 *   }
 * );
 */
export function useCustodiansList(variables?: CustodiansListVariables | VueCompositionApi.Ref<CustodiansListVariables> | ReactiveFunctionCustodiansList, baseOptions?: VueApolloComposable.UseQueryOptions<CustodiansList, CustodiansListVariables>) {
          return VueApolloComposable.useQuery<CustodiansList, CustodiansListVariables>(CustodiansListDocument, variables, baseOptions);
        }

export type CustodiansListCompositionFunctionResult = ReturnType<typeof useCustodiansList>;

Now, I aim to utilize it dynamically like this while minimizing repetition:

import * as Service from "./GQLService"; // from above
// e.g. typename = "custodian"
function useQueryList(typename:string) {
 const fnName = toFunctionName(typename) // e.g. useCustodiansList
 const result = Service[fnName](); //! this is the problem

 // Additionally, we seek to return everything including a parsedResult 
 const listName = `${typename}sList`
 return {
    [listName]: parseResult(result),
    ...result
  }
}

INTENT

I am determined not to duplicate the effort put into graphql-codgen by constructing a discriminated union TypeTable. My reasoning is based on the assumption that graphql-codegen has already handled this task effectively.

My ultimate objective is to enable individuals to create a new ExamplesList.graphql, whereby graphql-codegen processes it, making it readily accessible via useQueryList("example").

While the parameter is dynamically passed, it should also be feasible to obtain the correct static types by mapping the return types of all Service functions and determining the one that yields an Array<__typename>. Or might I be mistaken? Furthermore, I suspect I need to convert the typename parameter from a string to a string literal by extracting all potential __typenames from Service.

const result = Service[fnName](); //! this is the problem

This action represents just a fraction of our operations; we encapsulate and modify it further, but once the correct type is identified here, everything should fall into place smoothly.

Answer №1

In my opinion, this question delves more into the realm of TypeScript rather than GraphQL Codegen. Essentially, what you're attempting to achieve is dynamically obtaining a function property from an object, which might not be achievable with pure TypeScript without making modifications to the code generation output.

One approach could be developing a custom codegen plugin that generates an object derived from all your queries, featuring a single key of your choice (or perhaps just the operation name). This way, you can establish a connection between "example" and useExamplesListQuery.

Answer №2

Exploring the configuration provided caught my attention and I decided to experiment with it because it seemed very intriguing!.

In this scenario, some TypeScript sleuthing is required :) Using mapped types, I devised the solution below. As I wasn't sure about the functionality of your parsing function, I allowed it to return unknown but it should be an easy adjustment.

// The basic structure of a query result with __typename.
//
// Your example only dealt with lists,
// so I included the singular form for completeness :)
type QueryResultWithTypeName<T> = { __typename: T } | Array<{ __typename: T }>;

// Deriving a __typename (Custodian etc) from a query result (CustodiansList etc)
type TypeNameForResult<R> = NonNullable<
  {
    [K in keyof R]: NonNullable<R[K]> extends QueryResultWithTypeName<infer T> ? T : never;
  }[keyof R]
>;

// Determining a result property name (custodiansList etc) based on a query result object (CustodiansList etc)
type PropertyNameForResult<R> = NonNullable<
  {
    [K in keyof R]: NonNullable<R[K]> extends QueryResultWithTypeName<string> ? K : never;
  }[keyof R]
>;

// List of all available type names (Custodian etc)
type TypeName = {
  [K in keyof ServiceType]: ServiceType[K] extends () => UseQueryReturn<infer TResult, any>
    ? TypeNameForResult<TResult>
    : never;
}[keyof ServiceType];

// Mapping of type names (Custodian etc) and functions (useCustodianList etc)
//
// e.g. type UseCustodiansList = FunctionByTypeName['Custodian']
type FunctionByTypeName = {
  [K in TypeName]: {
    [L in keyof ServiceType]: ServiceType[L] extends () => UseQueryReturn<infer TResult, any>
      ? TypeNameForResult<TResult> extends K
        ? ServiceType[L]
        : never
      : never;
  }[keyof ServiceType];
};

// Mapping of type names (Custodian) and property names (custodiansList etc)
//
// e.g. type CustodianProperty = PropertyNameByTypeName['Custodian'] // will be 'custodiansList'
type PropertyNameByTypeName = {
  [K in keyof FunctionByTypeName]: FunctionByTypeName[K] extends () => UseQueryReturn<infer TResult, any>
    ? PropertyNameForResult<TResult>
    : never;
};

// Mapping of type names (Custodian) and function return types
//
// e.g. type CustodianProperty = ReturnTypeByTypeName['Custodian'] // will be UseQueryReturn<CustodiansList, CustodiansListVariables>
type ReturnTypeByTypeName = {
  [K in keyof FunctionByTypeName]: ReturnType<FunctionByTypeName[K]>;
};

// Type for the return object from useQueryList
// (I was not sure about the result of your parsing, hence using unknown)
//
// e.g. type UseCustodiansQueryReturnType = UseQueryListReturnType<'Custodian'> // will be { custodiansList: {}, /* the rest of UseQueryReturn */ }
type UseQueryListReturnType<T extends TypeName> = ReturnTypeByTypeName[T] &
  {
    [K in PropertyNameByTypeName[T]]: unknown;

    // It's recommended to keep the parsed result consistent across types, e.g. call it parsedResult:
    //
    // parsedResult: unknown;
  };

// A utility function to change 'Custodian' into 'custodian', enabling extraction of property name from type name later
const lowercaseFirstLetter = (value: string) => (value ? value[0].toLowerCase() + value.slice(1) : value);

// This section was undefined in your setup
const parseResult = <T>(a: T): T => a;

// Converting typename to a function
const toFunction = <T extends TypeName>(typename: T): FunctionByTypeName[T] => {
  // Initial type casting for string manipulation with types compatibility
  return Service[`use${typename}sList` as keyof ServiceType];
};

// Converting typename to property name (e.g. 'Custodian' => 'custodiansList')
const toPropertyName = <T extends TypeName>(typename: T): PropertyNameByTypeName[T] =>
  // Similar issue with string manipulation constraint
  `${lowercaseFirstLetter(typename)}sList` as PropertyNameByTypeName[T];

function useQueryList<T extends TypeName>(typename: T): UseQueryListReturnType<T> {
  const fn: FunctionByTypeName[T] = toFunction(typename); // e.g. useCustodiansList
  const result: ReturnTypeByTypeName[T] = fn(); //! this is the problem

  // Returning everything including a parsedResult
  const listName: PropertyNameByTypeName[T] = toPropertyName(typename);

  // Concerning third type casting which TypeScript finds tricky to agree upon that listName isn't just a string
  // but rather a unique string :)
  return {
    ...result,
    [listName]: parseResult(result),
  } as UseQueryListReturnType<T>;
}

Upon testing with:

const custodians = useQueryList('Custodian');

I can confirm that the usersList property is now accessible! Hooray!

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

Using Node.js, use the `require()` function to bring in external modules

In my development of an application using Typescript that compiles into node code, I find myself favoring import statements over require. When attempting to utilize Lodash with Lodash-Deep, the official documentation suggests using: const _ = require("dee ...

Creating an Array of Callbacks in TypeScript

How do you define an array of callback functions in TypeScript? Here's how a single callback function looks like: var callback:(param:string)=>void = function(param:string) {}; To declare an array of callbacks, you might try this: var callback: ...

Guide on utilizing a provider's variable in another provider within Ionic 2/3

In my code, I have a boolean variable called isConnection that is used to monitor network connection status. This variable is defined in a provider named 'network' and can be set to either true or false in different functions. Now, I need to acc ...

Transform a protractor screenshot into a PDF file

I'm currently working on a small Protractor code that captures screenshots, but my goal is to save these screenshots as PDF files. Below you can find the code snippet I have written. await browser.get(url); const img = await browser.takeScreenshot(); ...

Error: Unable to access the 'bitmap' property of an undefined object

Currently in the midst of extracting data from contentful using GraphQL within my Gatsby application. However, I have encountered the following error: Error: TypeError - unable to read bitmap of undefined. Assistance needed. I have successfully accesse ...

Can we send props to subcomponents in Inertia + Vue?

There are two components in my setup: Page.vue Component.vue In my Laravel route, I use: return inertia('Page', [ 'foo' => Model::all()->map(...) ]); Within Page.vue, I have the following code: <template> <Componen ...

Guide to validating a form using ref in Vue Composition API

Incorporating the Options API, I implemented form validation in this manner: template: <v-form ref="form" v-model="valid" lazy-validation @submit.prevent> ... script: methods: { validate() { this.$refs.form.validate(); ...

What is the best way to connect my Vue components with the active record objects in my Rails application?

Within a Rails project with Vue on the front-end, all views are powered by Vue while routing and models adhere to the Rails MVC pattern. Incorporating objects from Active Record into a Vue component is a goal. Additionally, there is a desire to create a V ...

Angular 4 Error: The function being called is not defined

I'm feeling stuck with my current project. I've come across similar questions asked multiple times in this thread and also here, but none of the solutions seems to fit my specific problem. It seems that the main issue lies in how this is referenc ...

Import external settings into vue.js

Currently working on an app that combines node.js and vue.js. I'm trying to figure out the best way to load environment variables into my vue.js components. So far, I've successfully loaded them in my server-side code using the dotenv package, bu ...

What is the best way to adjust the placement of a component to remain in sync with the v-model it is connected to?

I am encountering an issue with 2 sliders in my project. I have set it up so that when the lower slider's value is greater than 0, the top slider should automatically be set to 5. I am using a watcher function for this purpose. However, if I manually ...

What steps do I take to eliminate this additional content?

Currently working on a web messenger project and facing an issue with a specific bar that I want to remove from my interface: https://i.sstatic.net/sp05i.png Within the Home.blade.php file, the code snippet looks like this: @extends('layouts.texter& ...

Tips for converting a LOB (Binary image) for transmission to Firebase Storage

I am in need of retrieving Oracle images stored in a LOB field to then upload them to Firebase Storage. Currently, I am utilizing the oracledb library with TypeScript to invoke a procedure that fetches specific records. One of the fields returned is a LOB ...

Easy steps for integrating Vuex 3 with Vue 2.7

After using yarn create nuxt-app command, I noticed in the package.json file the following: "dependencies": { "nuxt": "^2.15.8", "vue": "^2.7.10", }, My project requires Vuex, specifically version 3 ...

Various types of generics within an object

Is there a way to achieve different types for the nested K type within a type like MyType? Here's an example: type Config<K> = { value: K; onUpdate: (value: K) => void; } type MyType<F extends string> = { [K in F]: <V>() =& ...

Angular 17 encountered a select error in core.mjs at line 6531 showing an unexpected synthetic listener error with code NG05105: @transformPanel.done

My select option is not working properly and I keep getting an error in the console that says: To resolve this issue, make sure to import either BrowserAnimationsModule or NoopAnimationsModule in your application. Here's my Typescript code: import { ...

Tips for resolving TypeScript issues with Vuex mapGetters

When utilizing mapGetters, TypeScript lacks insight into the getters linked to the Vue component, leading to error messages. For instance: import Vue from 'vue'; import { mapActions, mapGetters } from 'vuex'; export default Vue.extend ...

Breaking down type arguments in function declarations in TypeScript

Exploring a function that reverses a pair, I am seeking to define aliases for both the input and output in a way that can be utilized within the function itself and its type signature. function reverse<A, B>([a, b]: [A, B]): [B, A] { return [b, a] ...

What are some ways I can integrate phaser into my angular 4 project?

I'm embarking on the task of creating a game for a website using Angular 4 (or at minimum, version 2). Having experience in developing games with c++ and some knowledge of phaser along with basic HTML, CSS, and JavaScript, I find myself unsure of the ...

`Vue function is unable to find the definition of a variable`

I am struggling with utilizing a variable in a vue 3 application that has been emitted by a component called Mags. The variable is functioning properly in the importMags function, however, I am unable to access it within the handleSubmit function. It consi ...