Utilizing Typescript template strings for data inference

In coding practice, a specific convention involves denoting the child of an entity (meaning an association with another entity) with a '$' symbol.

class Pet {
  owner$: any;
}

When making a reference to an entity child, users should have the option to use either the full form ('owner$') or a simplified form ('owner').

One attempt at implementing this concept is shown below:

type ChildAttributeString = `${string}\$`;
type ShortChildAttribute<E> = ((keyof E) extends `${infer Att}\$` ? Att : never);
type ChildAttribute<E> = (keyof E & ChildAttributeString) | ShortChildAttribute<E>;

const att1: ChildAttribute<Pet> = 'owner$'; // Successfully matches the valid type
const att2: ChildAttribute<Pet> = 'owner'; // Successfully matches the valid type
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: 'previousOwner$' is not an attribute of Pet - As expected

This approach works fine when all attributes of Pet are considered child attributes. However, if a non-child attribute is added, the matching process breaks down:

class Pet {
  name: string;
  owner$: any;
}
const att1: ChildAttribute<Pet> = 'owner$'; // Successfully matches the valid type
const att2: ChildAttribute<Pet> = 'owner'; // INVALID: Type 'string' cannot be assigned to type 'never'
// To clarify: ChildAttribute<Pet> should accept values like 'owner' and 'owner$', but not 'name' which is not a child attribute (lacks the trailing '$')

What would be the appropriate types to ensure this functionality works as intended?

--- edit

To avoid confusion about the desired outcome and the definition of an "entity child", I have modified the question for clarity.

Answer №1

We iterate through the object keys, including both full and short forms for keys ending with a dollar sign symbol ($), while omitting others:

type ValuesOf<T> = T[keyof T]
type ChildAttribute<E> = 
  ValuesOf<{ [K in keyof E]: K extends `${infer Att}$` ? K | Att : never }>

interface Pet {
    name: string
    owner$: any
}

type PetAttr = ChildAttribute<Pet> // "owner$" | "owner"

Answer №2

ChildAttribute function should provide a combination of all permitted values.

type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )

// owner
type Test = RemoveDollar<'owner$'>

My interpretation suggests that if a value is accompanied by $, we can use a shortened getter, but if the property does not include $ like name, we cannot utilize name$ as a getter.

If my understanding is accurate, the following solution should suffice:

interface Pet {
  name: string;
  owner$: any;
}


type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )
// owner
type Test = RemoveDollar<'owner$'>

type WithDollar<T extends string> = T extends `${string}\$` ? T : never

// owner$
type Test2 = WithDollar<keyof Pet>

type ChildAttribute<E> = keyof E extends string ? RemoveDollar<keyof E> | WithDollar<keyof E> : never

const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected

Playground

RECURSION



type RemoveDollar<
  T extends string,
  Result extends string = ''
  > =
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends ''
      ? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
  )

/**
 * First cycle
 */

type Call = RemoveDollar<'foo$'>

type First<
  T extends string,
  Result extends string = ''
  > =
  // T extends        `{f}         {oo$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty
      // This branch is skipped on first iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'oo$', ${''}${f}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
 * Second cycle
 */
type Second<
  T extends string,
  // Result is f
  Result extends string = ''
  > =
  // T extends       `{o}$         {o$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty
      // This branch is skipped on second iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'o$', ${'f'}${o}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
* Third cycle
*/
type Third<
  T extends string,
  // Result is fo
  Result extends string = ''
  > =
  // T extends       `{o}          {$}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is not empty, it is $
      // This branch is skipped on third iteration         
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // RemoveDollar<'$', ${'fo'}${o}>
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

/**
* Fourth cycle, the last one
*/
type Fourth<
  T extends string,
  // Result is foo
  Result extends string = ''
  > =
  // T extends       `${$}        {''}
  (T extends `${infer Head}${infer Rest}`
    ? (Rest extends '' // Rest is  empty
      // Head is $           foo   
      ? (Head extends '$' ? Result : `${Result}${Head}`)
      // This branch is skipped on last iteration
      : RemoveDollar<Rest, `${Result}${Head}`>
    ) : never
  )

Playground

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

The type 'MutableRefObject<undefined>' cannot be assigned to the type 'LegacyRef<HTMLDivElement> | undefined'

I have created a customized hook that takes a ref object and observes its behavior: import { useState, useEffect, MutableRefObject } from "react"; const UseOnScreen = (ref: MutableRefObject<undefined>) => { const [isIntersecting, setI ...

Challenge Encountered with Create-React-App TypeScript Template: Generating JS Files Instead of TSX Files

Encountering a problem setting up a new React application with TypeScript using the Create-React-App template. Followed the guidelines on the official documentation (https://create-react-app.dev/docs/adding-typescript/) and ran the command below: npx creat ...

Populating datasets with relative indexing

I am working on a code where I need to fill the datasets with the property isProjected set to 1. There are 3 datasets - lower estimate, projected, and upper estimate. The goal is to fill the Lower Estimate and Upper Estimate with a background color of rgba ...

What is the method for locating an element within an array?

The content being returned is presenting a challenge. How can I retrieve data from inside 0? I attempted to access it using date[0] without success const { data } = getData(); The result of console.log(data) is shown below: enter image description here ...

The error message ``TypeError [ERR_UNKNOWN_FILE_EXTENSION]:`` indicates a

I am encountering an error while trying to run the command ./bitgo-express --port 3080 --env test --bind localhost: (node:367854) ExperimentalWarning: The ESM module loader is experimental. internal/process/esm_loader.js:90 internalBinding('errors ...

During the compilation process, Angular could not locate the exported enum

In the file models.ts, I have defined the following enum: export enum REPORTTYPE { CUSTOMER, EMPLOYEE, PROJECT } After defining it, I use this enum inside another class like so: console.log(REPORTTYPE.CUSTOMER); When I save the file, the IDE automati ...

Retrieve component information from the service

Incorporated within my component is a form that I'm utilizing with reactive form and my intention is to reach this form through my service. For example: const id = MyComponent.id and inside my component: @Output: public id: number = 7; ...

Typescript provides the flexibility to construct incomplete or partially valid objects

My attempt to create a partial helper function in Typescript led me to an incorrect version that went unnoticed by Typescript: Typescript version: 5.2.2 type A = { a: number; b: string }; // incorrect const buildPartialBad = (key: keyof A, val: A[keyof A ...

Utilizing React with .NET 8.0 and Minimal API, the front end is configured to send HTTP requests to its own specific port rather than the

Transitioning from working with react projects on .NET 3.1 to creating a new project on .NET 8.0 has been a challenging and confusing experience for me. When I attempted to access a newly created controller method, I encountered the error 404 Not Found. It ...

Changes on services do not affect the Angular component

Currently facing an issue with my Angular assignment where changing an element's value doesn't reflect in the browser, even though the change is logged in the console. The task involves toggling the status of a member from active to inactive and ...

The path referenced in typings is incorrect

I am currently facing an issue with my Typescript library that I am trying to publish on npmjs. It seems like the types file is not being exported correctly. The library has a simple method in the src/index.ts file and typings from src/typings/index.d.ts. ...

The TypeScript compilation failed for the Next.js module

Working on a project using Next.js with typescript. The dev server was running smoothly, and I could see frontend changes instantly. However, after modifying the next.config.js file and restarting the server (later reverting the changes), compilation issue ...

How to Delete Multiple Rows from an Angular 4 Table

I have successfully figured out how to remove a single row from a table using splice method. Now, I am looking to extend this functionality to remove multiple rows at once. html <tr *ngFor="let member of members; let i = index;"> <td> ...

What is the best way to save code snippets in Strapi for easy integration with SSG NextJS?

While I realize this may not be the typical scenario, please listen to my situation: I am using Strapi and creating components and collections. One of these collections needs to include code snippets (specifically typescript) that I have stored in a GitH ...

Exploring Service Injection and Factory Pattern in Angular 8

After going through various articles and official Angular guides, it appears that they were unable to assist me in solving my task. Here's what I wanted to achieve: Imagine having an Angular application with a product listing page. Additionally, this ...

Issue with loading CSS in Angular 8 upon refreshing the page after building in production

Here is the structure of my index.html: <!doctype html> <html lang="hu"> <head> <meta charset="utf-8"> <title>WebsiteName</title> <base href="/"> <meta name="viewport& ...

Troubleshooting: Angular add/edit form issue with retrieving data from a Span element within an ngFor loop

I am currently working on an add/edit screen that requires submitting a list, among other data. The user will need to check 2-3 checkboxes for this specific data, and the saved record will have multiple options mapped. Here is what the HTML looks like: &l ...

Encountering a Typescript error while trying to implement a custom palette color with the Chip component in Material-UI

I have created a unique theme where I included my own custom colors in the palette. I was expecting the color prop to work with a custom color. I tested it with the Button component and it performed as expected. However, when I attempted the same with the ...

Tips for correctly setting object initial values in React CreateContext

How can I correctly define the initial value of the constance trainsDetails in React Create Context? The trainsDetails is an object with various properties, fetched as a single object from an endpoint and has the values specified below in the TrainsDetails ...

The parameter type '==="' cannot be assigned to the 'WhereFilterOp' type in this argument

I'm currently working on creating a where clause for a firebase collection reference: this.allItineraries = firebase .firestore() .collection(`itinerary`); Here is the issue with the where clause: return this.allItiner ...