The Relationship between Field and Parameter Types in TypeScript

I am currently working on a versatile component that allows for the creation of tables based on column configurations.

Each row in the table is represented by a specific data model:

export interface Record {
  attribute1: string,
  attribute2: {
    subAttribute1: number,
    subAttribute2: {
      subSubAttribute1: string;
      subSubAttribute2: string;
    },
  },
}

The configuration of the table columns is determined using the ColumnType type.

type CustomType<T> = any; // The type of value extracted through the path

export interface ColumnType<T> {
  path: Paths<T>, // 'attribute1' | 'attribute2' | 'attribute2.subAttribute1' | 'attribute2.subAttribute2' | 'attribute2.subAttribute2.subSubAttribute1' | 'attribute2.subAttribute2.subSubAttribute2' 
  format?: (value: CustomType<T>) => string,
}

export type ColumnsType<T> = Array<ColumnType<T>>;

  • Path is utilized to extract the cell value from each row.
  • Format represents the function responsible for formatting the value derived from the row utilizing the specified path variable.

Column Configuration:

export const recordColumns: ColumnsType<Record> = [
  {
    path: 'attribute1',
  },
  {
    path: 'attribute2',
    format: ({subAttribute1, subAttribute2}) => subAttribute1 > 0 ? subAttribute2.subSubAttribute1 : subAttribute2.subSubAttribute2
  },
  {
    path: 'attribute2.subAttribute2',
    // format: ({subSubAttribute1, subSubAttribute2}: Record['attribute2']['subAttribute2']) => `${subSubAttribute1} - ${subSubAttribute2}`,
    format: ({subSubAttribute1, subSubAttribute2}) => `${subSubAttribute1} - ${subSubAttribute2}`,
  },
];

Question:

  1. How can I create the Magic type alias without explicitly defining the type in the configuration (e.g., Record ['attribute2'] ['subAttribute2'])?

Thank you for your assistance.

playground

Answer №1

To ensure that the path and format properties of ColumnType<T> are linked, you cannot achieve it when ColumnType<T> is a standalone interface. In your current definition:

export interface ColumnType<T> {
  path: Paths<T>, 
  format?: (value: Magic<T>) => string,
}

The type Paths<T> represents a union of all possible paths from T, and regardless of what Magic<T> is, there's no way for it to be related to segments of Paths<T>.

What you really need is for ColumnType<T> itself to be a combination of path/format pairs, each corresponding to a path in Paths<T>. This implies discarding separate types like Paths<T> and Magic<T>, and instead trying to integrate them.

Here's one potential solution:

// Join<K, P> adds key K to path P with a dot 
//   (unless either K or P are empty)
type Join<K, P> = K extends string | number
  ? P extends string | number
  ? `${K}${'' extends P ? '' : '.'}${P}`
  : never
  : never;

// PathFormat<P, V> denotes the path/format pair for path P and value V
type PathFormat<P, V> = { path: P, format?: (value: V) => string }

// PrependToPathFormat<K, PF> takes a key K and an existing
//   PathFormat PF and generates a new PathFormat where key K
//   is added to the path.
type PrependToPathFormat<K, PF> =
  PF extends PathFormat<infer P, infer V> ? PathFormat<Join<K, P>, V> : never

// ColumnType<T> contains the union of PathFormat<K, T[K]> for 
//  every key K in keyof T and, recursively, the result of adding K 
//  to the PathFormat union obtained from ColumnType<T[K]>
type ColumnType<T> = T extends object ? { [K in keyof T]-?:
  PathFormat<K, T[K]> | (PrependToPathFormat<K, ColumnType<T[K]>>)
}[keyof T] : never

The concept behind this approach is that ColumnType<T> will encompass PathFormat<K, T[K]> for each key K and value T[K] in T, along with a modified version of ColumnType<T[K]>.


Let's illustrate with an example:

interface SimpleObject { c: number, d: boolean }
type ColumnTypeSimple = ColumnType<SimpleObject>;
// type ColumnTypeSimple = 
//  PathFormat<"c", number> | 
//  PathFormat<"d", boolean>

interface NestedObject { a: string, b: SimpleObject }
type ColumnTypeNested = ColumnType<NestedObject>;
// type ColumnTypeNested = 
//  PathFormat<"a", string> | 
//  PathFormat<"b", SimpleObject> | 
//  PathFormat<"b.c", number> | 
//  PathFormat<"b.d", boolean>

As seen above, ColumnType<SimpleObject> combines PathFormat types for the c and d properties of SimpleObject. Furthermore, ColumnType<NestedObject> integrates PathFormat types for the a and b properties of NestedObject, along with ColumnType<SimpleObject> adjusted so that the paths "c" and "d" become "b.c" and "b.d" respectively.


Now let's evaluate your itemsColums code, which operates correctly:

const itemsColumns: ColumnsType<Item> = [
  {
    path: 'field1',
  },
  {
    path: 'field2',
    format: ({ field21, field22 }) => field21 > 0 ? field22.field221 : field22.field221
  },
  {
    path: 'field2.field22',
    format: ({ field221, field222 }) => `${field221} - ${field222}`,
  },
]; // okay

We will also detect errors where the path or format are incorrect or do not align properly:

const badItemsColumns: ColumnsType<Item> = [
  { path: "field3" }, // error! 
  //~~~~ <--
  //Type '"field3"' is not assignable to type 
  //'"field1" | ... | "field2.field22.field222"'.

  { path: "field2.field21", format: (({ field222 }) => "") }, // error!
  // ---------------------------------> ~~~~~~~~
  // Property 'field222' does not exist on type 'Number'.
];

PLEASE NOTE that such recursive mapped types may exhibit unusual edge cases, so extensive testing is recommended to ensure coverage of various scenarios. Depending on the specific type T passed into ColumnType<T>, unexpected outputs, recursion limits, or performance issues with the compiler might arise. There are strategies to address these concerns, but addressing them comprehensively beforehand exceeds the scope of a single Stack Overflow inquiry.

Link to Playground for Code Testing

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

How can you notify a component, via a service, that an event has occurred using Subject or BehaviorSubject?

For my Angular 10 application, I created a service to facilitate communication between components: export class CommunicationService { private messageSubject = new Subject<Message>(); sendMessage(code: MessageCode, data?: any) { this.messag ...

The issue of "ReferenceError: Cannot access '<Entity>' before initialization" occurs when using a OneToMany relationship with TypeORM

There are two main actors involved in this scenario: User and Habit. The relationship between them is defined by a OneToMany connection from User to Habit, and vice versa with a ManyToOne. User Entity import {Entity, PrimaryGeneratedColumn, Column, Creat ...

Can someone explain the distinction between 'return item' and 'return true' when it comes to JavaScript array methods?

Forgive me for any errors in my query, as I am not very experienced in asking questions. I have encountered the following two scenarios :- const comment = comments.find(function (comment) { if (comment.id === 823423) { return t ...

Utilizing React forwardRef with a functional component

Looking at my code, I have defined an interface as follows: export interface INTERFACE1{ name?: string; label?: string; } Additionally, there is a function component implemented like this: export function FUNCTION1({ name, label }: INTERFACE1) { ...

When I try to ng build my Angular 11 application, why are the type definitions for 'Iterable', 'Set', 'Map', and other types missing?

As someone new to Angular and the node ecosystem, I'm facing a challenge that may be due to a simple oversight. The issue at hand: In my Angular 11 project within Visual Studio 2019, configured with Typescript 4, attempting to run the project throug ...

The display of the selected input is not appearing when the map function is utilized

I am attempting to use Material UI Select, but it is not functioning as expected. When I use the map function, the default value is not displayed as I would like it to be. However, it does work properly when using the traditional method. *** The Method th ...

The Art of Validating Configurations Using io-ts

I have recently switched to using io-ts instead of runtypes in a fresh project. In terms of configuration validation, my approach involves creating an object that specifies the types for each part of the config; const configTypeMap = { jwtSecret: t.str ...

Tips for ensuring only one property is present in a Typescript interface

Consider the React component interface below: export interface MyInterface { name: string; isEasy?: boolean; isMedium?: boolean; isHard?: boolean; } This component must accept only one property from isEasy, isMedium, or isHard For example: <M ...

"Creating a backend server using Node.js, TypeScript, and g

I am currently in the process of developing a nodejs project that will consist of 3 key services: Gateway Product Order The Product and Order services will perform functions related to their respective names, while the Gateway service will take JSON requ ...

A guide to effectively utilizing a TypeScript cast in JSX/TSX components

When trying to cast TypeScript in a .tsx file, the compiler automatically interprets it as JSX. For example: (<HtmlInputElement> event.target).value You will receive an error message stating that: JSX element type 'HtmlInputElement' is ...

Encountering a compilation error due to a Typescript assignment

While working with Typescript, I encountered a compilation error in the code shown below: console.log('YHISTORY:login: data = '+data); let theData = JSON.parse(data); console.log('YHISTORY:login: theData = '+JSON.stringify(theData)); ...

The 'type' property within the NGRX Effect is not present in the type Observable<any[]>

I am currently in the process of upgrading my Angular app from version 6 to version 7. Additionally, I am upgrading the TypeScript version from 2.7.2 to 3.1.6. The issue I'm encountering is that TypeScript is flagging an error stating that my ngrx ef ...

What is the process for importing a JSON file into a TypeScript script?

Currently, I am utilizing TypeScript in combination with RequireJS. In general, the AMD modules are being generated flawlessly. However, I have encountered a roadblock when it comes to loading JSON or any other type of text through RequireJS. define(["jso ...

Having difficulty implementing dynamic contentEditable for inline editing in Angular 2+

Here I am facing an issue. Below is my JSON data: data = [{ 'id':1,'name': 'mr.x', },{ 'id':2,'name': 'mr.y', },{ 'id':3,'name': 'mr.z', },{ & ...

Exploring Angular 4's capability: Incorporating data from Http Post Response into a .js file or highchart

I'm a beginner with Angular 4. I'm trying to create a dashboard that pulls data from an Http Post Response and I want to use this data to create a Chart (Highchart). I have successfully received the response in the console.log and formatted it i ...

What is the method to access the information within the observer?

When I receive the data from the observer in the console, here is what I see: https://i.stack.imgur.com/dVzwu.png However, I am only interested in extracting this specific data from each item on the list: https://i.stack.imgur.com/g8oHL.png To extract ...

implementing an event listener in vanilla JavaScript with TypeScript

Can anyone help me figure out how to correctly type my event listener in TypeScript + Vanilla JS so that it has access to target.value? I tried using MouseEvent and HTMLButtonElement, but I haven't been successful. const Database = { createDataKeys ...

Can you tell me the appropriate type for handling file input events?

Using Vue, I have a simple file input with a change listener that triggers the function below <script setup lang="ts"> function handleSelectedFiles(event: Event) { const fileInputElement = event.target as HTMLInputElement; if (!fileInp ...

Showing records from Firebase that are still within the date range

I'm currently developing an application that allows users to book appointments on specific dates. After booking, I want the user to only be able to view appointments that are scheduled for future dates. I've attempted to compare the date of each ...

What are the steps for implementing persisting and rehydrating data in redux-toolkit?

After setting up the redux-persist with react-toolkit as recommended in the documentation, I found myself needing to perform some operation on rehydrate. Unfortunately, my attempts have been unsuccessful so far. Here is what I have tried: ... import { RE ...