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?