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

Need to import a JSON file and convert it to an interface in order to ensure that all fields are included

I am facing an issue where I am attempting to parse a json file using require(). My goal is to convert it into a specific data type and have the conversion fail if the file does not contain all the necessary fields as defined by the interface. Below is my ...

Each property of an object has its own unique key, yet they all share the same data type

I have a single-use object with only three properties, all of which should be of the same type. The code below currently achieves this, but I'm curious if there is a more efficient way to declare the type for timingsObject: let timingsObject: ...

Mismatched URLSearchParam type {property: value} does not match with undefined and Argument Type {property: value} cannot be assigned to {property: value}

Every time I try to run the code below, I encounter this error. The code is supposed to take a refresh token as input and make a POST request using URLSearchParams to a specific URL. However, I keep getting an error related to URLSearchParams. https://i.s ...

Looking to categorize and sum the values within an array of objects using JavaScript?

I'm looking to optimize this response data within my Angular application. res=[ { "url": "/page1", "views": 2 }, { "url": "/page2", "views": 1 }, { "url": "/page1", "views": 10 }, { "url": "/page2", "views": 4 }, { "url": "/page3", "views": 1 }, ...

Encountering a problem with the 'string' parameter when using TypeScript

I keep encountering the following error message: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ barkingRoadProject: string[]; }'. No index signature with a paramet ...

Exporting modules in TypeScript using "module.exports"

While working on my Yeoman generator, I initially wrote it in JavaScript like this: "use strict"; var Generator = require("yeoman-generator"); var chalk = rquire("chalk"); module.exports = class extends Generator { initializing() { this.log( c ...

The functionality of the System JS map is not functioning properly

Despite the challenges I face with System.js, I find it to be a valuable tool that I prefer over alternatives. This is my current System.js configuration: System.config({ packages: { app: { format: 'register' ...

Ways to establish the relationship between two fields within an object

These are the definitions for two basic types: type AudioData = { rate: number; codec: string; duration: number; }; type VideoData = { width: number; height: number; codec: string; duration: number; }; Next, I need to create a MediaInfo typ ...

Creating a currency input field in HTML using a pattern textbox

In a project using HTML, Angular 2, and Typescript, I am working with a textbox. How can I ensure that it only accepts numbers along with either one dot or one comma? The input should allow for an infinite number of digits followed by a dot or a comma and ...

Classify the array types based on their indexes within the map

Is there a way to efficiently access elements in TypeScript using a map with the correct type? When attempting this, an error is thrown due to the type from the map being number | string type Tuple = [number, number, string]; const y: Tuple = [1, 2, &apos ...

Disabling FormArray on-the-fly in Angular

I have a scenario where I need to disable multiple checkboxes in a FormArray when the page loads. Despite my attempts to implement this, it hasn't been successful so far. Can someone provide guidance on how to achieve this? .ts file public myForm: Fo ...

Challenges with variable scopes and passing variables in Ionic 2 (Typescript)

In my Ionic 2 TypeScript file, I am facing an issue with setting the value of a variable from another method. When I close the modal, I get undefined as the value. I'm encountering difficulty in setting the value for coord. export class RegisterMapP ...

Create an HTML button on the homepage that directs users to the "about" page

I've been struggling to make a button in my Ionic app navigate to a different page upon clicking. Despite being new to Ionic, I've spent hours trying to solve this issue. Below is the HTML code in home.page.html: <ion-header> &l ...

Issue: Angular 7 unable to route directly to URL with specific ID

When I click on the navigation link with code for button click, it works perfectly fine: this.router.navigate(['/dashboard', this.selectedDateString]); However, if I manually type the URL into the address bar like this: I receive a forbidden! ...

What is the process for creating a unit test case for an Angular service page?

How can I create test cases for the service page using Jasmine? I attempted to write unit tests for the following function. service.page.ts get(): Observable<Array<modelsample>> { const endpoint = "URL" ; return ...

Using Typescript in the browser with Babel and Webpack: Step-by-Step Guide

I've been exploring the use of Typescript in my browser with a specific architecture: Typescript in browser architecture However, I'm facing an issue with the import/export functionality when running this command: tsc && babel build-ts -d lib && ...

Running two different wdio.config.js files consecutively

Is it possible to run two wdio.config.js files with different configurations, one after another? Here is how the first configuration file is defined in the code: const { join } = require('path'); require('@babel/register') exports.co ...

Using TypeScript to destructure by providing types

I encountered an issue while trying to destructure some code. The error message Property 'name' does not exist on type '{}'. is appearing. I thought about using let user:any = {}; as a workaround, but that goes against the eslint rule o ...

Indeed, conditional validation is essential

I have encountered an issue with my schema validation where I am trying to validate one field based on another. For example, if the carType is "SUV", then the maximum number of passengers should be 6; otherwise, it should be 4. However, despite setting u ...

What is the best way to execute a function on the output of *ngFor directive in Angular 2?

Imagine having a list of all the users within your system: allUsers = { a: {name:'Adam',email:'<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="39585d5854794d5c4a4d5a56175a56... f: {name:'fred' ...