Exploring generic types using recursive inference

The scenario:

export type SchemaOne<T> =
  | Entity<T>
  | SchemaObjectOne<T>;
export interface SchemaObjectOne<T> {
  [key: string]: SchemaOne<T>;
}
export type SchemaOf<T> = T extends SchemaOne<infer R> ? R : never;

const sch: Entity<ArticleResource> = ArticleResource.getEntitySchema();
const a = { a: sch  };
const aa: SchemaOne<ArticleResource> = a;
// it's functioning!
type Z = SchemaOf<typeof a>;
// Z is ArticleResource as expected

const b = { a: { b: sch }  };
type ZZ = SchemaOf<typeof b>;
// ZZ is unknown - SADBEAR

I have managed to correctly match my recursive definition (borrowed from https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540). (hence the works after aa definition). However, I am now trying to infer the type without making it more generic (getting bb's type).

For some reason, this only seems to work one level deep. Is this a limitation of TypeScript? Is there a way to utilize recursion with infer to actually determine the generic type?

Answer №1

Understanding how the compiler infers conditional types can be quite complex. It seems that there is a limit to how many type instantiations it will perform before defaulting to unknown or any. If you find that infer R alone doesn't provide the desired result, consider creating your own method to analyze a type and determine the appropriate return value for R.

In this scenario, let's define some basic types to get started on compiling...

// Hypothetical definitions

interface Entity<T> {
  e: T;
}

class ArticleResource {
  static getEntitySchema<T>(): Entity<T> {
    return null!;
  }
  ar = "ArticleResource";
}

One potential implementation of SchemaOf<T> could look like this:

type _SchemaOf<T> = T extends Entity<infer R>
  ? R
  : T extends object ? { [K in keyof T]: _SchemaOf<T[K]> }[keyof T] : never;

(Named _SchemaOf as it will be used to construct the final SchemaOf). This logic checks if T is simply an Entity<R>, returning R. If not, it attempts to recursively iterate through object properties to extract Entity types and merge them into a union.

This function should ideally return what you expect - for example, _SchemaOf<Entity<A>> yields A, and

_SchemaOf<{a: X, b: Y, c: Z}>
results in
_SchemaOf<X> | _SchemaOf<Y> | _SchemaOf<Z>
, capturing all Entity instances within an object.

However, it may yield unexpected outcomes when provided with non-SchemaOne<T> types, like {a: Entity<A>, b: string}, which returns just A. Therefore, a verification step is necessary:

type SchemaOf<T> = T extends SchemaOne<_SchemaOf<T>> ? _SchemaOf<T> : never;

This check ensures that the result from _SchemaOf<T> aligns with

SchemaOne<_SchemaOf<T>>
. If not, the output is set to never.

To test these implementations:

type Z = SchemaOf<typeof a>;
// Expected output: ArticleResource

const b = { a: { b: sch } };
type ZZ = SchemaOf<typeof b>;
// Expected output: ArticleResource

const c = { a: { b: sch, c: "string" } };
type _C = _SchemaOf<typeof c>; // ArticleResource
type C = SchemaOf<typeof c>; // never

The above tests demonstrate the behavior of the implemented functions and ensure that they are functioning as intended. Hopefully, this provides some clarity and helps you progress. Good luck!

Link to code

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

Why does Typescript's 'await' seem to not wait as expected?

Apologies for the rookie mistake, I am currently transitioning from a C# background to Ionic, which may be causing some confusion on my end. I'm working on retrieving a stored token from Ionic storage but I'm struggling with understanding promise ...

Avoid the need for props when implementing a component with a higher order component

Running into an issue with HOC and typescript. The compiler is asking for a value that is received from the HOC. Here's the component using the HOC: function Coupon(props: WithAlertProps): JSX.Element { return <p>test {props.error}</p> } ...

A guide on assigning specific (x, y) coordinates to individual IDs within the tree structure

When attempting to calculate the positions of each ID in order to arrange them hierarchically on the canvas, I encounter some challenges. Whether it's organizing them into a tree structure or multiple trees resembling a forest, one restriction is that ...

Having trouble with the npm Fluid Player installation

I am attempting to integrate Fluid Player into my Angular application Using - npm i fluid-player However, I'm encountering this error ...

When I utilize the redux connect function, the class information in my IDE (PhpStorm/WebStorm) seems to disappear

When I use the connect function from redux, it seems to hinder my IDE (PhpStorm) from "Find Usages" on my classes. This is likely because connect returns any, causing the type information from the imported SomeClass file to be lost. export default connect ...

Encountering errors when examining local variables during unit testing on an Angular component

If we have 2 components, namely AppComponent and TestComponent. The TestComponent is being called using its directive in the HTML template of the AppComponent. Within the TestComponent, there is an @Input() property named myTitle. Unit testing is being pe ...

TypeORM Error: Trying to access property 'findOne' of an undefined object

I've been working on implementing TypeORM with Typescript, and I have encountered an issue while trying to create a service that extends the TypeORM repository: export class UserService extends Repository<User> { // ... other service methods ...

What are the TypeScript type definitions for the "package.json" configuration file?

What is the most efficient method for typing the content of the "package.json" file in TypeScript? import { promises as fs } from 'fs'; export function loadManifest(): Promise<any> { const manifestPath = `${PROJECT_DIR}/package.json`; ...

Guide to importing a class property from one file to another - Using Vue with Typescript

Here is the code from the src/middlewares/auth.ts file: import { Vue } from 'vue-property-decorator' export default class AuthGuard extends Vue { public guest(to: any, from: any, next: any): void { if (this.$store.state.authenticated) { ...

What causes different errors to occur in TypeScript even when the codes look alike?

type Convert<T> = { [P in keyof T]: T[P] extends string ? number : T[P] } function customTest<T, R extends Convert<T>>(target: T): R { return target as any } interface Foo { x: number y: (_: any) => void } const foo: Foo = c ...

Encountering a module error when using SignalR with Webpack and TypeScript: 'Unable to locate module './NodeHttpClient''

I am currently working on integrating a SignalR client into an Angular application using Webpack and TypeScript. Here is the content of my package.json file: { "private": true, "version": "0.0.0", "scripts": { "test": "karma start ClientApp/tes ...

Enhancing User Authentication: Vue 3 with TypeScript Login

Recently, I came across a new technology called Supabase and noticed that most resources mention registration on JavaScript instead of TypeScript. As I started working on a project using Vue 3 + TypeScript, I encountered some errors that I need help resolv ...

Subscription Code Incrementally Triggering Upon Each Component Load

Within the initialization of my component, I have the following code: public Subscription: Subscription; ngOnInit() { this.subscription = this.myService.currentData.subscribe( dataReceived => { this.data = dataReceived; this.useDa ...

Modify the JSON format for the API POST request

I need assistance with making an API POST call in Angular 8. The JSON object structure that needs to be sent should follow this format: -{}JSON -{}data -[]exp +{} 0 +{} 1 However, the data I am sending is currently in this format: - ...

How to exit a dialog in an Angular TypeScript component with precision

Hey there, I'm attempting to close a dialog from the component by specifying the path in .angular-cli.json and calling the function. However, it seems that despite my efforts, the dialog isn't closing and the redirection isn't happening. He ...

Converting JSON to TypeScript in an Angular project

Within my Angular project, I have an HTTP service that communicates with a web API to retrieve JSON data. However, there is a discrepancy in the naming convention between the key in the JSON response (e.g., "Property" in uppercase) and the corresponding pr ...

Issue: Unable to assign value to 'googleUri' property of null. Resolving with Interface for two-way binding?

Can anyone help me figure out why I keep getting a 'set property of null' error while attempting 2way binding in my interface? Whenever I submit the form and trigger the onSave function, I encounter the error "Cannot set property 'googleUri ...

Removing AWS-CDK Pipelines Stacks Across Multiple Accounts

Currently, I am utilizing pipelines in aws-cdk to streamline the process of automating builds and deployments across various accounts. However, I have encountered an issue where upon destroying the pipeline or stacks within it, the respective stacks are ...

What's the best way to replicate a specific effect across multiple fields using just a single eye button?

Hey everyone, I've been experimenting with creating an eye button effect. I was able to implement one with the following code: const [password, setPassword] = useState('') const [show, setShow] = useState(false) <RecoveryGroup> ...

Are there any comparable features in Angular 8 to Angular 1's $filter('orderBy') function?

Just starting out with Angular and curious about the alternative for $filter('orderBy') that is used in an AngularJS controller. AngularJS example: $scope.itemsSorted = $filter('orderBy')($scope.newFilteredData, 'page_index&apos ...