Issue
I would like to declare type for object (root
), that contains nested array of objects (values
), where each object has properties one
(any type) and all
(array of one
‘s type).
Bellow is my attempt to do it, however I don’t know how to obtain types of one
property, so there is just Value<any>
:
type Value<T> = { one: T, all: T[] }
type Root = { values: Value<any>[] }
const root: Root = {
values: [
{ one: 1, all: 5 }, // Should be error, 'all' should be an array of numbers.
{ one: true, all: [5] }, // Should be error, 'all' should be array of booleans.
{ one: 'a', all: ['a', 'b', 'c'] }, // Ok.
{ one: true, all: [false, true, true] } // Ok.
]
}
Is there any way how to do that? Here is example. It would be amazing, if it could be without naming all possible combinations of types like:
type Value = { one: string, all: string[] } | { one: number, all: number[] } | { one: boolean, all: boolean[] }
because it should be applicable to any type.
Solution
As you note, your Root
value is too wide to enforce the constraint you care about, but it does have the advantage of being straightforward to use:
const root: Root = {
values: [
{ one: true, all: [5] }, // uh oh no error
{ one: 'a', all: ['a', 'b', 'c'] }, // okay
{ one: true, all: [false, true, true] } // okay
]
}
const indexes = root.values.map(v => v.all.indexOf(v.one));
console.log(indexes) // [-1, 0, 1]
I’m showing the above for comparison purposes. The following approach will enforce the constraint, but you will end up paying for that with some loss of convenience.
When you find yourself wishing you could write an infinite union type like the invalid
type SomeValue = Value<string> | Value<number> | Value<boolean>
| Value<null> | Value<Date> | Value<{a: string, b: number}> | ...
it’s a sign that you are looking for existentially quantified generic types. Most languages with generics, including TypeScript, only directly support universally quantified generic types (which correspond to infinite intersection types). There is a feature request at microsoft/TypeScript#14466 to support existentially quantified generics, but it’s not part of the language yet.
Still, it is possible to emulate these generics. The difference between existential and universal generics has to do with who gets to specify the type parameter. If you switch the role of data supplier and data consumer, then universals become existentials. So we can encode SomeValue
like this:
type SomeValue = <R>(cb: <T>(value: Value<T>) => R) => R;
Let’s say you have a value someValue
of type SomeValue
. If you want to access the underlying Value<T>
data, you need to call someValue()
with some callback cb
that receives the Value<T>
and does something with it. It’s like an immediately resolved Promise
. The cb
callback must be prepared for any possible Value<T>
value; whoever supplies value
gets to choose what T
is. All you can say is that it’s some T
.
You can write a helper function to turn any Value<T>
into a SomeValue
:
const toSomeValue = <T,>(value: Value<T>): SomeValue => cb => cb(value);
And then your Root
would be
type Root = { values: SomeValue[] }
Which means you can now create root
as follows:
const root: Root = {
values: [
toSomeValue({ one: true, all: [5] }), // error!
toSomeValue({ one: 'a', all: ['a', 'b', 'c'] }), // okay
toSomeValue({ one: true, all: [false, true, true] }) // okay
]
}
Here you’ve got the type checking you wanted (with the penalty that you needed to write toSomeValue()
a bunch of times). And now you can make indexes
from before by pushing your old v => v.all.indexOf(v.one)
callback down into someV
:
const indexes = root.values.map(someV => someV(v => v.all.indexOf(v.one)));
console.log(indexes) // [-1, 0, 1]
So it produces the same result, but again, with more complexity.
Now you might want to try to make Root
generic itself, where you map an array type to an array of Value<T>
for individual T
types. And then you could either manually annotate that root
is, say, Root<[5, "a" | "b" | "c", boolean]>
, or try to get the compiler to infer [5, "a" | "b" | "c", boolean]
from the initializer to the values
property. These approaches are technically possible, but they add even more complexity than the existential type encoding above, and they are not as type safe. So I won’t go into detail here; although the code is included in the link at the bottom of the answer.
Answered By – jcalz
This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0