TS: Obtain specific typing for each generic child

Picture a scenario where the type of one property is determined by the value of another property

For example, there is a planet with two countries: if your child is born in 'onename land', their name should be a single string; but if they are born in 'twonames land', their name should consist of two strings

const iPeople: { nationality: 'onename land' | 'twonames land', name: string | [string, string] }[] = [
  { nationality: 'onename land', name: 'Jon' },
  { nationality: 'twonames land', name: ['Jon', 'Snow'] }
]

for generic purposes...

const iSomeThings: { a: 'X' | 'Y', b: string | [string, string] }[] = [
  { a: 'X', b: 'lorem' },
  { a: 'Y', b: ['lorem', 'ipsum'] }
]

In the case of both iSomeThing objects, the inferred type of 'b' is 'string | [string, string]', but I want it to be either 'string' or '[string, string]', based on the value of 'a'

iSomeThings[0].b // inferred type is "string | [string, string]" but I want it to be "string" since 'a' value is 'X'
iSomeThings[1].b // inferred type is "string | [string, string]" but I want it to be "[string, string]" since 'a' value is 'Y'

I want the type of 'b' property to depend on the value of 'a' property

If 'a' is 'X', then 'b' should be a string, and if 'a' is 'Y', then 'b' should be a tuple of two strings... let's do it:

type A = 'X' | 'Y'
type B<T extends A> = T extends 'X' ? string : T extends 'Y' ? [string, string] : never

interface ISomeThing<T extends A> {
  a: T
  b: B<T>
}

class SomeThing<T extends A> implements ISomeThing<T> {
  a: T
  b: B<T>

  constructor(private _iSomething: ISomeThing<T>) {
    this.a = this._iSomething.a
    this.b = this._iSomething.b
  }
}

Example using a specific 'a' type:

const someThing1: SomeThing<'X'> = new SomeThing<'X'>({ a: 'X', b: 'lorem' })
const someThing2: SomeThing<'Y'> = new SomeThing<'Y'>({ a: 'Y', b: ['lorem', 'ipsum'] })
someThing1.b // ✅ type of b is 'string'
someThing2.b // ✅ type of b is '[string, string]'

Example using a generic 'a' type:

const someThing1: SomeThing<A> = new SomeThing<A>({ a: 'X', b: 'lorem' })
const someThing2: SomeThing<A> = new SomeThing<A>({ a: 'Y', b: ['lorem', 'ipsum'] })
someThing1.b // ❌ type of 'b' is 'string | [string, string]', but I'm expecting it to be 'string'
someThing2.b // ❌ type of 'b' is 'string | [string, string]', but I'm expecting it to be '[string, string]'

Example using 'map' to set the generic from each object

const iSomeThings: ISomeThing<A>[] = [{ a: 'X', b: 'lorem' }, { a: 'Y', b: ['lorem', 'ipsum'] }]
const someThings: SomeThing<A>[] = iSomeThings.map(iSomeThing => new SomeThing<typeof iSomeThing.a>(iSomeThing))

someThings[0].b // ❌ type of 'b' is 'string | [string, string]', but I'm expecting it to be 'string'
someThings[1].b // ❌ type of 'b' is 'string | [string, string]', but I'm expecting it to be '[string, string]'

Now, let's delve into what's happening

<typeof iSomeThing.a>

⚠️ Offtopic

If I remove the 'typeof' keyword, the generic argument would be interpreted as a value (e.g. 'X'), when it actually needs to be a type to avoid TS error. Should I create a new type based on the value of 'a'?

⚠️ Assumption

The type of 'a' is 'A' (either 'X' or 'Y'), which is why the type of 'b' is 'string | [string, string]'. To stay focused on the question, let's assume this is fixed. Now...

The children types of someThings[n] should be accessed individually, how can I ensure this while still utilizing the parent type array?

Answer №1

In the quest for a more efficient solution that minimizes overhead, I have decided to proceed with the following strategy

const iSomeThings: ISomeThing<A>[] = [{ a: 'X', b: 'lorem' }, { a: 'Y', b: ['lorem', 'ipsum'] }]
const someThings: (SomeThing<'X'> | SomeThing<'Y'> )[] = iSomeThings.map(iSomeThing => {
  if (iSomeThing.a === 'X') return new SomeThing<'X'>(iSomeThing as ISomeThing<'X'>)
  else if (iSomeThing.a === 'Y') return new SomeThing<'Y'>(iSomeThing as ISomeThing<'Y'>)
})

someThings.forEach(someThing => {
  switch (someThing.a) {
    case 'X':
      // ✅ someThing.value is string
      break;
    case 'Y':
      // ✅ someThing.value is [string, string]
      break;
  }
})

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 attribute 'location' is not found in the 'File' data type

Recently, I made the switch from using multer to multer-s3 in my project. I have updated the controllers accordingly to store the "location" instead of the "filename". However, I am encountering a typescript error. At the moment, I am utilizing Localstack ...

Implementing Formik in React for automatic updates to a Material-UI TextField when blurred

Presently, I am developing a dynamic table where users can simultaneously modify multiple user details in bulk (Refer to the Image). The implementation involves utilizing Material-UI's <TextField/> component along with Formik for managing form s ...

What is the process in TypeScript for defining a custom variation of a generic function?

Suppose we have a generic function: const f1 = <T>(x: T) => console.log(x) We can then create a specialized version for f1, like this: const f2 = (x: number) => f1(x) If we try to call f2 with an argument of type string, TypeScript will thr ...

Navigating through alterations in intricate data utilizing the BehaviorSubject concept

If I have a Service that offers a BehaviorSubject to any component needing the most up-to-date version of certain data, how can these components differentiate what changed in the data when they receive notifications? export class DataService { pri ...

A reference to 'this' is not permissible within a static function in Types

Based on this GitHub issue, it is stated that referencing this in a static context is allowed. However, when using a class structure like the following: class ZController { static async b(req: RequestType, res: Response) { await this.a(req) ...

Placing gaps after every group of four digits

I am currently working with Ionic 4 and Angular 8, attempting to insert a space every 4 digits entered by the user. However, the function I have written seems to be malfunctioning, as it inserts a space after each action the user takes following 4 numbers. ...

`Is there a way to assign multiple parameters to HttpParams within Angular 5?`

I'm attempting to send multiple parameters to HttpParams in Angular 5 using the approach below: paramsObject: any params = new HttpParams(); for (let key in paramsObject) { params.set(key, paramsObj ...

Tips for properly waiting for an AngularFire2 subscription to complete before executing the subsequent lines of code within Angular2

Having an issue with my Angular2 Type Script code. The goal is to access Questions from a Firebase Database by subscribing to a FirebaseListObserver: this.af.list('/questions').subscribe( val => { this.questions = val console.log(th ...

I built a custom Angular application integrated with Firebase, and I'm looking for a way to allow every user to have their individual table for managing tasks

I am looking to enhance my code so that each user who is logged in has their own tasks table, which they can update and delete. Additionally, I need guidance on how to hide menu items tasks, add-tasks, logout for users who are not logged in, and display th ...

Issue with Angular Material: Default selection not being applied in mat-select component

When I have a mat-select with options defined as objects in an array, I am facing an issue where the default selected value is not being set when the page renders. In my TypeScript file, I have: public options2 = [ {"id": 1, "name": "a"}, {"id": 2 ...

The Angular checked functionality is not working as expected due to the presence of ngModel

Just getting started with Angular here. I’m working on a checkbox table that compares to another table and automatically checks if it exists. The functionality is all good, but as soon as I add ngModel to save the changes, the initial check seems to be ...

`In Vue.js, it is not possible to access a data property within a

I encountered an issue while attempting to utilize a property of my data within a computed method in the following manner: data() { return { ToDoItems: [ { id: uniqueId("todo-"), label: "Learn Vue", done: false }, ...

I am searching for answers to solve issues related to a JSON file

I am currently working on a tool that searches for matches in an input field by comparing the keywords entered by the user with a JSON. During my testing phase, I focused on using a single API that provides information about different countries and fortun ...

Create a fake version of ElementRef and simulate the getBoundingClientRect() function

Looking to simulate ElementRef and access getBoundingClientRect() I experimented with the code below, but it didn't yield the expected results. const mockHostRef = { nativeElement: { getBoundingClientRect: {top: 10, left: 10} } } {pr ...

International replacement of external interface exportation

Currently, I am utilizing the @react-native-firebase/messaging library and invoking the method messaging().onNotificationOpenedApp(remoteMessage => ....) I am aiming to replace the property data within the type of remoteMessage in order to benefit from ...

how can JavaScript be used to retrieve an object based on a condition from an array of objects and an ArrayList

My JavaScript challenge involves working with an array of objects called arrobj and a list called prgList. The goal is to extract the names from arrobj based on the programs listed in prgList. If the program exists in arrobj, it should be displayed accor ...

The combination of Material UI and React Hook Form is encountering an issue with submitting disabled checkboxes

My current challenge involves ensuring that all fields are disabled while the form is submitting. I have successfully implemented this for text, selects, and radios, but I am facing difficulties with required checkboxes. I am working with nextjs typescrip ...

Tips for adding and verifying arrays within forms using Angular2

Within my JavaScript model, this.profile, there exists a property named emails. This property is an array composed of objects with the properties {email, isDefault, status}. Following this, I proceed to define it as shown below: this.profileForm = this ...

Eliminate apostrophes in a string by using regular expressions

Can someone help me with this word What is The Answer? I need to eliminate the space and apostrophe '. To remove spaces, should I use this method: data.text.replace(/ +/g, ""). But how can I remove the apostrophe? ...

`How to Merge Angular Route Parameters?`

In the Angular Material Docs application, path parameters are combined in the following manner: // Combine params from all of the path into a single object. this.params = combineLatest( this._route.pathFromRoot.map(route => route.params) ...