Typescript opting for union type inference over specified type inference

My goal is to integrate "metadata" into a type for the purpose of developing a type-safe REST client. The concept involves using the type metadata within the link to automatically identify the correct endpoint schema for API calls. For example:

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type User = {
  self: Link<"users">;
};

const user: User = { self: "https://..." };

http(user.self, "GET", { userId: 1 });

I managed to achieve this using conditional types in a somewhat brute-force manner.

For instance:

type Routes = "users" | "posts";
type Verbs<R> = R extends "users" ? "GET" : never;
type Query<R, V> = R extends "users"
  ? V extends "GET"
    ? { queryId: string }
    : never
  : never;

However, this approach resulted in a normalized type model that would be cumbersome to input manually. Instead, I aim to utilize a de-normalized type structure, like so:

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

This can be implemented with types such as:

type Query<
  S,
  RN extends keyof S,
  VN extends keyof S[RN]
> = OpQuery<S[RN][VN]>;

I have almost successfully executed all elements of this plan, except for one crucial aspect - deducing the route name from the link type:

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type Link<R extends keyof Schema> = string;

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

type name = LinkRouteName<Link<"users">>;

Expected outcome: name === "users"

Actual result: name === "users" | "posts"

Answer №1

The type system of TypeScript is based on structural typing rather than nominal typing, where the structure of a type determines its identity instead of the name of the type. For example, a type alias like:

type Link<R extends keyof Schema> = string

does not create distinct types based on the value of R. Both Link<"users"> and Link<"posts"> ultimately evaluate to string, making them interchangeable within the type system. While there are rare cases where the compiler may treat these types differently due to their names, it's not something you should rely on.

Information about the type R is discarded in expressions such as:

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

Both

LinkRouteName<Link<"users">>
and
LinkRouteName<Link<"posts">>
reduce to LinkRouteName<string>, providing no further specifics beyond the generic constraint on R in the definition of Link<R>.

To establish distinction between two types, they must have disparate structures. Though for primitive types like string, altering their structure at runtime isn't feasible (e.g., adding properties dynamically). One potential workaround involves utilizing "branded primitives" by intersecting a phantom property with the primitive type:

type Link<S extends keyof Schema> = string & { __schema?: S };

This approach permits creating values like:

const userLink: Link<"users"> = "anyStringYouWant";

However, manual type annotation is necessary for the compiler to recognize such values accurately.


In defining functions like http(), consider using tuple types to capture dynamic parameters based on the schema:

declare function http<
  S extends keyof Schema,
  V extends keyof Schema[S],
  >(
    url: Link<S>,
    verb: V,
    ...[query]: Schema[S][V] extends { query: infer Q } ? [Q] : []
  ): void;

This setup leverages rest tuple types to support conditional parameter acceptance determined by the corresponding schema entry's properties.

A practical application of this would be:

type User = { self: Link<"users"> };
const user: User = { self: "https://..." };
http(user.self, "GET", { userId: "1" }); // okay

type Post = { self: Link<"posts"> }
const post: Post = { self: "https://..." }
http(post.self, "POST"); // okay 

By ensuring proper type annotations and structuring your code accordingly, TypeScript can handle such scenarios effectively. Good luck with your coding endeavors!

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

A function that creates a new object with identical keys as the original input object

I am working on creating a function fn() that has the following specifications: It takes a single argument x which is an object with optional keys "a" and "b" (each field may be numeric for simplicity) The function should return a new object with the same ...

Angular 13: Issue with Http Interceptor Not Completing Request

In my app, I have implemented a HtppInterceptor to manage a progress bar that starts and stops for most Http requests. However, I encountered an issue with certain api calls where the HttpHandler never finalizes, causing the progress bar to keep running in ...

Exploring the Differences Between Native Script and Ionic: A Guide to Choosing the Right Framework for Your Hybrid Apps

When deciding between Ionic and Native Script for hybrid app development, which technology would you recommend? Or do you have another suggestion knowing that I am familiar with Angular 6? Also, I am looking for a Native Script tutorial, preferably in vide ...

How to set return types when converting an Array to a dynamic key Object in Typescript?

Can you guide me on defining the return type for this function? function mapArrayToObjByKeys(range: [string, string], keys: { start: string; end: string }) { return { [keys.start]: range[0], [keys.end]: range[1] } } For instance: mapArrayToObj ...

What is the best way to include an object in a document before sending it back?

I am facing a challenge that involves retrieving data from Firestore using angularfire. Once I fetch a collection, I need to iterate through each document in the collection and then make a separate request to Firestore to get additional document values. Th ...

Organizing JSON keys based on their values using Typescript

In the context of a main JSON structure represented below, I am interested in creating two separate JSONs based on the ID and Hobby values. x = [ {id: "1", hobby: "videogames"}, {id: "1", hobby: "chess"}, {id: "2", hobby: "chess ...

Issue: The module '@nx/nx-linux-x64-gnu' is not found and cannot be located

I'm encountering issues when trying to run the build of my Angular project with NX in GitHub Actions CI. The process fails and displays errors like: npm ERR! code 1 npm ERR! path /runner/_work/myapp/node_modules/nx npm ERR! command failed npm ERR! c ...

The intricate nature of a generic asynchronous return type hinders the ability to accurately deduce precise

My goal in this coding playground is to create a versatile API handler helper that guarantees standard response types and also utilizes inference to ensure our application code can effectively handle all potential scenarios: Visit the Playground However, ...

Get items from an array that have a property matching another array

Is there a more efficient way to create a new array with contact objects that have values matching those in selectedContact? selectedContact: number[] = [0,2] //value contacts: Contact[] = [{ firstName:"Dan"; lastName:"Chong"; email:"<a href="/c ...

Ensuring a component stays active while navigating in Angular 2

Currently, I have a javascript application that heavily relies on jquery. It's not the most visually appealing, difficult to maintain, and definitely in need of a framework upgrade. That's why I'm in the process of migrating it to be compati ...

Guide to setting up a one-to-many self relation entry in Prisma

I am facing a challenge with a simple schema model that includes one-to-many self relations. In this scenario, my goal is to create a parent entity along with its children in a single transaction. How can I accomplish this task effectively? data-model Y{ ...

Array automatically updates its values

.... import * as XLSX from 'xlsx'; ... I am currently parsing a .xlsx file in an Ionic 4 application. showData() { let fileReader = new FileReader(); fileReader.onloadend = (e) => { this.arrayBuffer = fileReader.result; let data ...

Utilizing Angular 9's inherent Ng directives to validate input components within child elements

In my current setup, I have a text control input component that serves as the input field for my form. This component is reused for various types of input data such as Name, Email, Password, etc. The component has been configured to accept properties like ...

The interface is incompatible with the constant material ui BoxProps['sx'] type

How can I make the interface work for type const material ui? I tried to register an interface for sx here, but it keeps giving me an error. import { BoxProps } from '@mui/material'; interface CustomProps { sx: BoxProps['sx&apo ...

How can resolvers in GraphQL optimize data fetching based on necessity?

I am working with two unique GraphQL types: type Author { id: String! name: String! } type Book { id: String! author: Author! name: String! } Within my database structure, there exists a foreign key inside the books table: table authors (pseu ...

What is the best way to connect a series of checkboxes within a form utilizing Angular?

I created a form with checkboxes that allow users to select multiple options. However, when I submit the form, instead of receiving an array of objects representing the checked checkboxes, I'm not getting anything at all. Here is what I see in the co ...

Adding dynamic metadata to a specific page in a next.js app using the router

I was unable to find the necessary information in the documentation, so I decided to seek help here. My goal is to include metadata for my blog posts, but I am struggling to figure out how to do that. Below is a shortened version of my articles/[slug]/page ...

Using 'dom-autoscroller' in a TypeScript file results in an error message stating that the module cannot be found

Currently, I am facing an issue while trying to incorporate 'dom-autoscroller' in conjunction with dragula within my Angular 4/Typescript (v2.5) project. Despite successfully installing the 'dom-autoscroller' npm package, I encounter a ...

What is the method for accessing the string value of a component's input attribute binding in Angular 2?

In my Angular2 application, I have a straightforward form input component with an @Input binding pointing to the attribute [dataProperty]. The [dataProperty] attribute holds a string value of this format: [dataProperty]="modelObject.childObj.prop". The mod ...

Tips for converting necessary constructor choices into discretionary ones after they have been designated by the MyClass.defaults(options) method

If I create a class called Base with a constructor that needs one object argument containing at least a version key, the Base class should also include a static method called .defaults() which can set defaults for any options on the new constructor it retu ...