In TypeScript, deduce the optional generic type automatically

Feeling a bit out of my depth here. I need to perform an inference on a generic that includes an optional "parse" function which returns the formatted value [or throws].

They say code speaks louder than words, so let's take a look at the example:

export type RouteDefInput<TInput = unknown> = {
  parse: (input: unknown) => TInput;
};
export type RouteDefResolver<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = (opts: { ctx: TContext; input: TInput }) => Promise<TOutput> | TOutput;

export type RouteDef<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = {
  input?: RouteDefInput<TInput>;
  resolve: RouteDefResolver<TContext, TInput, TOutput>;
};

export type inferRouteInput<
  TRoute extends RouteDef<any, any, any>
> = TRoute['input'] extends RouteDefInput<any>
  ? ReturnType<TRoute['input']['parse']>
  : undefined;

const context = {};
function createRoute<TInput, TOutput>(
  route: RouteDef<typeof context, TInput, TOutput>
) {
  return route;
}

function stringParser(input: unknown): string {
  if (typeof input === 'string') {
    return input;
  }
  throw new Error('not a string');
}

const myRoute1 = createRoute({
  input: {
    parse: stringParser,
  },
  resolve({ input }) {
    return {
      output: input,
    };
  },
});

const myRoute2 = createRoute({
  resolve({ input }) {
    return {
      output: input,
    };
  },
});

// this should render MyRouteInput1 as "string"
type MyRouteInput1 = inferRouteInput<typeof myRoute1>;

// this should render MyRouteInput2 as "undefined" (works)
type MyRouteInput2 = inferRouteInput<typeof myRoute2>;

Can anyone help me figure out how to properly get the inferRouteInput to work in both cases?

I've been struggling with this for quite a while - you can test it out yourself in the TypeScript playground: TypeScript Playground

Your assistance is greatly appreciated! 🙏

If you have any recommendations for further reading on this topic or on TypeScript generics in general, please feel free to share as I'm having trouble finding reliable sources.

Answer №1

After some trial and error, I managed to get everything up and running smoothly. The approach I took may be considered somewhat forceful - by overloading the createRoute function. Here is the code snippet that did the trick:

export type RouteDefInput<TInput = unknown> = {
  parse: (input: unknown) => TInput;
};
export type RouteDefResolver<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = (opts: { ctx: TContext; input: TInput }) => Promise<TOutput> | TOutput;

// export type RouteDef<
//   TContext = unknown,
//   TInput = unknown,
//   TOutput = unknown
// > = {
//   input?: RouteDefInput<TInput>;
//   resolve: RouteDefResolver<TContext, TInput, TOutput>;
// };

export type RouteDefWithInput<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = {
  input: RouteDefInput<TInput>;
  resolve: RouteDefResolver<TContext, TInput, TOutput>;
};

export type RouteDefWithoutInput<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = {
  resolve: RouteDefResolver<TContext, TInput, TOutput>;
};
export type RouteDef<TContext = unknown, TInput = unknown, TOutput = unknown> =
  | RouteDefWithInput<TContext, TInput, TOutput>
  | RouteDefWithoutInput<TContext, TInput, TOutput>;

export type inferRouteInput<
  TRoute extends RouteDef<any, any, any>
> = TRoute extends RouteDef<any, infer Input, any> ? Input : never;

const context = {};
function createRoute<TInput, TOutput>(
  route: RouteDefWithInput<typeof context, TInput, TOutput>,
): RouteDefWithInput<typeof context, TInput, TOutput>;
function createRoute<TInput, TOutput>(
  route: RouteDefWithoutInput<typeof context, TInput, TOutput>,
): RouteDefWithoutInput<typeof context, TInput, TOutput>;
function createRoute(route: any) {
  return route;
}

function stringParser(input: unknown): string {
  if (typeof input === 'string') {
    return input;
  }
  throw new Error('not a string');
}

const myRoute1 = createRoute({
  input: {
    parse: stringParser,
  },
  resolve(input) {
    return {
      output: input,
    };
  },
});

const myRoute2 = createRoute({
  resolve({ input }) {
    return {
      output: input,
    };
  },
});

// this should render MyRouteInput1 as "string"
type MyRouteInput1 = inferRouteInput<typeof myRoute1>;

// this should render MyRouteInput2 as "unknown" (works)
type MyRouteInput2 = inferRouteInput<typeof myRoute2>;

Answer №2

This piece of code is functioning correctly:

export type RouteDefinitionInput<TInput = unknown> = {
  parse: (input: unknown) => TInput;
};
export type RouteDefinitionResolver<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = (options: { ctx: TContext; input: TInput }) => Promise<TOutput> | TOutput;

export type RouteDefinition<
  TContext = unknown,
  TInput = unknown,
  TOutput = unknown
> = {
  input?: RouteDefinitionInput<TInput>;
  resolve: RouteDefinitionResolver<TContext, TInput, TOutput>;
};

export type inferRouteInputType<
  TRoute extends RouteDefinition<any, any, any>
> = TRoute extends RouteDefinition<any, infer TInput, any>
  ? NonNullable<TInput>
  : undefined;

const contextOptions = {};
function createRouteDefinition<TInput, TOutput>(
  route: RouteDefinition<typeof contextOptions, TInput, TOutput>
) {
  return route;
}

function parseString(inputData: unknown): string {
  if (typeof inputData === 'string') {
    return inputData;
  }
  throw new Error('Input data is not a string');
}

const firstRoute = createRouteDefinition({
  input: {
    parse: parseString,
  },
  resolve({ input }) {
    return {
      output: input,
    };
  },
});

const secondRoute = createRouteDefinition({
  resolve({ input }) {
    return {
      output: input,
    };
  },
});

// Result should display MyFirstRouteInput as "string"
type MyFirstRouteInput = inferRouteInputType<typeof firstRoute>;

// Result should display MySecondRouteInput as "undefined" (verified)
type MySecondRouteInput = inferRouteInputType<typeof secondRoute>;

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

What is the best way to create a TypeScript interface or type definition for my constant variable?

I'm facing challenges in defining an interface or type for my dataset, and encountering some errors. Here is the incorrect interfaces and code that I'm using: interface IVehicle { [key: number]: { model: string, year: number }; } interface IV ...

Discovering the permissible array values from numerous arrays inside an object using TypeScript

I have an object with multiple arrays of strings (hardcoded). I want to specify that only strings from that object are allowed in another empty array. In a simple non-nested scenario, I'm achieving this with typeof someArray[number][]. So, I hoped to ...

Explaining the union type using a list of data types

Is there a way to create a union type that strictly limits values to 'a', 'b', 'c' when using a list like const list: string[] = ['a', 'b', 'c']? I know one method is: const list = ['a' ...

A data type that exclusively accepts values from an enumerated list without mandating the inclusion of every possible value within the enum

Here's a code snippet I'm working with: enum Foo { a, b, c } type Bar = { [key in keyof typeof Foo]: string; } const test: Bar = { a: 'a', b: 'b' }; I'm encountering an issue where the code is complaining ...

Enhance your React Typescript High Order Component by incorporating additional properties and implementing them

I am in the process of creating a React HOC with specific requirements: It should take a component as input, modify the hidden property (or add it if necessary), and then return the updated component The rendered component should not display anything whe ...

Utilizing Typescript for Efficient Autocomplete in React with Google's API

Struggling to align the types between a Google address autocomplete and a React-Bootstrap form, specifically for the ref. class ProfileForm extends React.Component<PropsFromRedux, ProfileFormState> { private myRef = React.createRef<FormContro ...

The ongoing ESLint conundrum: Balancing between "Unused variable" and "Unknown type" errors when utilizing imports for type annotations

I've encountered a linting issue and I need some guidance on how to resolve it. Here's the scenario - when running $ yarn lint -v yarn run v1.22.4 $ eslint . -v v6.8.0 With plugins vue and @typescript-eslint, I have the following code in a .ts ...

Deciphering key-value pairs that are separated by commas

I am looking to convert the following format: realm="https://api.digitalocean.com/v2/registry/auth",service="registry.digitalocean.com",scope="registry:catalog:*" Into this JSON object: { realm: "https://api.digitaloce ...

Eslint is back and it's cracking down on unused variables with no

I've configured eslint to alert me about unused variables rules: { '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], } Presently, I have a TypeScript class structured like this: import { User } from &ap ...

What is the best way to show an error message if a TextInput field is left blank?

I'm currently working on a mobile application using react-native, focusing on the login page. My goal is to show an error message below a TextInput field when it's left empty. To achieve this, I've been experimenting with the @react-hook-f ...

Issue: The CSS loader did not provide a valid string output in Angular version 12.1.1

I am encountering 2 error messages when trying to compile my new project: Error: Module not found: Error: Can't resolve 'C:/Users/Avishek/Documents/practice/frontend/src/app/pages/admin/authentication/authentication.component.css' in &apos ...

The CORS policy specified in next.config.js does not appear to be taking effect for the API request

I am currently working on a Next.js application with the following structure: . ├── next.config.js └── src / └── app/ ├── page.tsx └── getYoutubeTranscript/ └── getYoutubeTranscript.tsx T ...

Error message stating 'is not recognized' caused by Angular SharedModule

I have a navbar component that I've organized into a module called 'NavbarModule' so that it can be easily shared. My goal is to use this component in the 'ProductsListComponent'. However, even after properly importing and exportin ...

Issue with service injection within a singleton service in Angular2

Currently, I have two services in my application. ServiceA is non-singleton and provided to components through the Providers array, while ServiceB is a singleton that is listed in its module's Providers array. Both services work perfectly fine indepen ...

Angular mat-table experiencing issues with matToolTip functionality

My Angular project is using Angular Material 16x, but for some reason, the matToolTip is not displaying at all. I have experimented with various versions, including a basic matTooltip="hello world", but I just can't seem to get it to work. I have come ...

Preventing me from instantiating objects

I've been struggling with an issue for a while now consider the following: export abstract class abstractClass { abstract thing(): string } export class c1 extends abstractClass { thing(): string { return "hello" } } export cla ...

Encountered a type error during project compilation: "Type '(e: ChangeEvent<{ value: string[] | unknown; }>) => void' error occurred."

I recently started working with react and I'm facing an issue with a CustomMultiSelect component in my project. When I try to call events in another component, I encounter the following error during the project build: ERROR: [BUILD] Failed to compile ...

Having trouble receiving accurate intellisense suggestions for MongoDB operations

Implementing communication between a node application and MongoDB without using Mongoose led to the installation of typing for both Node and MongoDB. This resulted in the creation of a typings folder with a reference to index.d.ts in the server.ts file. In ...

Sharing information between different components in React can be done using props, context, or a

When attempting to edit by clicking, the parent information is taken instead of creating a new VI. I was able to achieve this with angular dialog but I am unsure how to do it with components. This is done using dialog: <div class="dropdown-menu-item" ...

Is there a way to dynamically create a property and assign a value to it on the fly?

When retrieving data from my API, I receive two arrays - one comprising column names and the other containing corresponding data. In order to utilize ag-grid effectively, it is necessary to map these columns to properties of a class. For instance, if ther ...