Recursive object types in Typescript allow for defining complex data structures

I am looking to create a type/interface that can store properties of the same type as itself.

For Instance:

    type TMessagesFormat = { [key: string]: string };
    
    interface TMessages {
      messages: TMessagesFormat;
    }
    
    interface TGroupMessages {
      messages?: TMessagesFormat;
      controls: { [key: string]: TMessages | TGroupMessages }
    }
    
    let groupMessages: TGroupMessages = {
      controls: {
        username: { messages: {required: 'Username required'} }
      }
    }
    
    let messages: TGroupMessages = {
      controls: {
        username: { messages: { required: 'Username required' } },
        passwordGroup: {
          messages: { nomatch: 'Passwords doesn't match' },
          controls: {
            password: { messages: { required: 'Password required' } }
          }
        }
      }
    };

The type checking is functioning correctly for username and passwordGroup, but the controls in passwordGroup can currently be anything without triggering any errors from the TS compiler. Even if I add the property controls: 'whatever' (which should not be a valid type) inside the username object literal, the code still compiles without any warnings or errors. How is this possible? Thank you!

Answer №1

With the release of version 3.7, TypeScript has transitioned into a new realm of possibility. Initially criticized for being too strict, the language has evolved to strike a balance between rigidity and flexibility. The decision to start off stringent and gradually relax restrictions was a strategic move by the language developers, ultimately resulting in a more versatile TS v3.7. The adjustments made to the typescript compiler have allowed for more recursive type aliases, paving the way for intricate and layered type definitions.


While there are numerous examples to showcase this shift, one standout demonstration is the newfound freedom in defining recursive types.

type TMessagesFormat = { [key: string]: TMessagesFormat };

interface TMessages extends TMessagesFormat{
   [key: string]: TMessagesFormat;
}

interface TGroupMessages {
   messages?: TMessagesFormat;
   controls: { [key: string]: TMessages | TGroupMessages };
}

The above TypeScript code is now completely valid

Answer №2

It appears that there is an issue in TypeScript where excess property checks on object literals do not behave as expected for unions. The following code snippet demonstrates this:

interface A {
    a: string;
}
interface B {
    b: number;
}
const a: A = {a: 'dog', b: 'cat'}; // error, because 'b' is an unknown property
const ab: A | B = {a: 'dog', b: 'cat'}; // no error

One would expect ab to throw an error such as "'cat' is not a number", but it does not due to the aforementioned issue. If and when this issue is resolved, your problem should be resolved as well.


However, we do not necessarily have to wait for a fix. Excess property checking on object literals is not overly strict; it simply warns you if you add an unknown property to a "fresh" object literal. In TypeScript, adding extra properties to an object that already conforms to a certain interface is generally allowed. So, if you find a way to bypass the excess property checking (for instance, by assigning a "non-fresh" literal), you can freely add additional properties.

const dogCat = {a: 'dog', b: 'cat'};
const a: A  = dogCat; // no error

If you truly wish to disallow extra properties, particularly ones with known key names, there is a method:

interface A {
    a: string;
    b?: never; // cannot have a defined 'b'
}
interface B {
    b: number;
}
const dogCat = {a: 'dog', b: 'cat'};
const a: A  = dogCat; // error, 'string' is not assignable to 'undefined'
const ab: A | B = {a: 'dog', b: 'cat'}; // error, 'string' is not assignable to 'number'

This will result in errors for both non-fresh instances of a and ab. Returning to your scenario...


If we assume that TMessages cannot contain a controls property:

interface TMessages {
  messages: TMessagesFormat;
  controls?: never; // no defined 'controls'
}

interface TGroupMessages {
  messages?: TMessagesFormat;
  controls: { [key: string]: TMessages | TGroupMessages }
}

In this case, if a TMessages | TGroupMessages has a specified controls property, it must adhere to the type defined in TGroupMessages:

let tm: TMessages | TGroupMessages = {messages: {foo: 'hey'}, controls: 3}
// error, 'number' is not assignable to '{ [k: string]: TMessages | TGroupMessages }'

This approach should address your issue. Best of luck!


TL;DR

Either await resolution of Microsoft/TypeScript#22129, or modify your TMessages interface as follows:

interface TMessages {
  messages: TMessagesFormat;
  controls?: never; // no defined 'controls'
}

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

angular determine if a column exists within a matCellDef iteration

Is there a way to check a column in an ng for loop in Angular without using the logic "{{element[p.key] != null ? '$' : ''}}" for a specific column or excluding a specific column? View image description here #html code <table mat-t ...

What methods can be used to specify the shape of this variable within typescript callback functions?

I'm currently in the process of transitioning an existing project from using mongoose on nodejs to Typescript. I've hit a roadblock when it comes to defining the shape of 'this' in some of the callback functions. For instance, my user o ...

Having trouble retrieving attributes from Typed React Redux store

After scouring Google, official documentation, and Stack Overflow, I am still struggling to access my Redux store from a React functional component (written in Typescript). Despite following various guides, I keep encountering undefined errors. I can retri ...

Issue with Typescript and react-create-app integration using Express

I'm relatively new to Typescript and I decided to kickstart a project using create-react-app. However, I encountered an issue while trying to connect my project to a server using express. After creating a folder named src/server/server.ts, React auto ...

Modifying the property value based on the selected item from a dropdown menu in Angular 2

I am brand new to Angular 2 and I have come across the following code snippet. <select name="shape_id" (change)="changeShape()"> <option *ngFor="let shape of shapes" [ngValue]="shape.name"> {{shape.name}} </option> </s ...

Asynchronous handling of lifecycle hooks in TypeScript for Angular and Ionic applications

I'm intrigued by the idea of integrating TypeScript's async/await feature with lifecycle hooks. While this feature is undeniably convenient, I find myself wondering if it's considered acceptable to make lifecycle hooks asynchronous. After ...

Ways to utilize Subjects for sharing global information in Angular 6

I have been struggling to find an effective way to share data between two components that have the same parent in an Angular application. Currently, I am working with an Angular Material stepper where Step 1 contains one component and Step 2 contains anot ...

What is the best method for combining two observables into one?

My goal is to initialize a list with 12 users using the URL ${this.url}/users?offset=${offset}&limit=12. As users scroll, the offset should increase by 8. I plan to implement infinite scrolling for this purpose. However, I am facing an issue with appen ...

An issue occurred while attempting to pretty-print JSON in Typescript

Currently, I am attempting to utilize https://www.npmjs.com/package/prettyjson in conjunction with Typescript; however, it is unable to locate the module. To begin, I created a package.json file: { "name": "prettyjson-test", "description": "prettyjso ...

React Native Expo Launch disregards typescript errors

While using Expo with Typescript, I've noticed that when I run the app with expo start or potentially build it, TypeScript errors are being ignored. Although Visual Studio Code still flags these errors, I can still reload the App and run it on my pho ...

Converting an existing array into a TypeScript string literal type: A step-by-step guide

Converting TypeScript Arrays to String Literal Types delves into the creation of a string literal type from an array. The question raised is whether it's feasible to derive a string literal from an existing array. Using the same example: const furnit ...

Passing a retrieved parameter from the URL into a nested component in Angular

I'm currently facing an issue where I am trying to extract a value from the URL and inject it into a child component. While I can successfully retrieve the parameter from the URL and update my DOM with a property, the behavior changes when I attempt t ...

Using React.useMemo does not efficiently avoid unnecessary re-renders

In my code, I am working with a simple component as shown below: const Dashboard = () => { const [{ data, loading, hasError, errors }] = useApiCall(true) if (hasError) { return null } return ( <Fragment> <ActivityFeedTi ...

The Measure-x Annotation toolbar in High stocks is conspicuously absent from the graph, instead making an appearance in the navigator section below

Recently, I encountered a strange issue that has left me puzzled. When I trigger the measure-x event on my graph, the annotation and its toolbar are not displaying on the graph as expected. Instead, they appear on the navigator below. The annotation should ...

When using TypeScript, how can I effectively utilize the Component property obtained from connect()?

Software Development Environment TypeScript 2.8 React 16 Redux Foo.tsx interface IFooProps{ userId:number } class Foo extends Component<IFooProps>{ render(){ return <p>foo...</p> } } const mapStateToProps = (state: I ...

Is it possible in Typescript to reference type variables within another type variable?

Currently, I am working with two generic types - Client<T> and MockClient<T>. Now, I want to introduce a third generic type called Mocked<C extends Client>. This new type should be a specialized version of MockClient that corresponds to a ...

Refreshing an Angular page when navigating to a child route

I implemented lazy loading of modules in the following way: { path: 'main', data: {title: ' - '}, component: LandingComponent, resolve: { images: RouteResolverService }, children: [ { path: '', redirectTo: 'home&apo ...

Tips for sending the image file path to a React component

Hey, I'm working on a component that has the following structure: import React from "react"; interface CInterface { name: string; word: string; path: string; } export function C({ name, word, path }: CInterface) { return ( < ...

Having trouble accessing the property 'prototype' of null in Bing Maps when using Angular4

I'm currently working on creating a Bing component in Angular 4, but I'm facing issues with rendering the map. Below is my index.html file: <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title> ...

Ways to bring in a Typescript Vue project to a Javascript Vue

I am trying to incorporate this Calendar component into my Javascript Vue 3 project. To achieve this, I have created a new component in my project named ProCalendar.vue and copied the code from the example found in App.vue. Additionally, I have added the n ...