One problem that makes this particular example hard to discuss is that, on the face of it, a value foo of type (n: A) => boolean (where A is some unresolved generic type parameter that extends Json) cannot safely be called. I can't call foo(123) because while 123 extends Json, I would need to know that 123 extends A, which I don't know. A is a mystery to the caller of that function. It's as if the function is saying "I want something but I won't tell you what it is", and unless you use the any type or type assertions, TypeScript's type system won't even let you guess. In what follows I will ignore this problem and just use type assertions, but it would have been nicer if the question had something more motivating (e.g., a type like {val: A, consumer(val: A): void;} with an unknown A would be usable because you could always safely pass the object's val to its own consumer method). Oh well.
Anyway, whenever you find yourself wishing you had an "infinite union type", it's a sign that you want to use existentially-quantified generics. If these existed in TypeScript, you could write something like:
// not valid TS, don't try this
type SomeFooArray = Array< <∃A extends Json> Foo<A> >;
where <∃A extends Json>(...) means "there exists an A assignable to Json for which the enclosed expression holds, but I don't know or care what A is".
Unfortunately, like most languages with generics, TypeScript only directly supports universally-quantified generics, which acts as an infinite intersection, where <A extends Json>(...) means "the enclosed expression holds for all possible A types that are assignable to Json".
There is an open feature request for existential types at microsoft/TypeScript#14466, but who knows if it'll ever be implemented.
One thing about universal-vs-existential quantification, and unions-vs-intersections is that they are duals to each other, and that one thing can be transformed into its dual by changing the role of data producer and data consumer. It therefore turns out that you can emulate existential types by representing them as a Promise-like data structure. Instead of handing you a <∃A extends Json> Foo<A>, I hand you a
type SomeFoo = <R>(cb: <A extends Json>(foo: Foo<A>) => R) => R;
It's a function that holds its Foo in a black box, and you can pass callbacks into it. The function calls the callback with its Foo as a parameter and hands you back the return result. Anything you could do with a <∃A extends Json> Foo<A> could be done with a SomeFoo:
function acceptSomeFooArray(someFooArray: SomeFoo[]) {
return someFooArray.map(someFoo => someFoo(foo => foo("hmm"))); // boolean, but
// -------------------------------------------------> ~~~~~
// '"hmm"' is assignable to the constraint of type 'A',
// but 'A' could be instantiated with a different subtype of constraint 'Json'.
}
Oh, darn, there's that error because you cannot safely call it. Fine:
function acceptSomeFooArray(someFooArray: SomeFoo[]) {
return someFooArray.map(someFoo => someFoo(
(<A extends Json>(foo: Foo<A>) => foo("hmm" as A))) // assert
);
}
Anyway, it's easy enough to turn a concrete Foo<A> into a SomeFoo:
const toSomeFoo = <A extends Json>(foo: Foo<A>): SomeFoo => cb => cb(foo);
And so you could pass an array of SomeFoo where an array of <∃A extends Json> Foo<A> is wanted. As long as you can change your data structures, this is a viable solution.
Oh, but you have an existing library whose data structures you can't alter. In that case, it is still possible to use an alternative of switching the producer with the consumer. Instead of trying to come up with a specific type that means "Foo<A> for some A" and apply it to pieces of data, you instead go to any library function which needs to deal with data of this sort and make it generic so it can handle a Foo<A> for all A.
For example, if you have a library function processFooArray that accepts a heterogeneous array of "some" Foo, you can declare it this way:
type DistribFoo<T> = T extends Json ? Foo<T> : never;
// declaration
declare function processFooArray<A extends readonly Json[]>(
arr: readonly [...{ [K in keyof A]: DistribFoo<A[K]> }]): [...{ [K in keyof A]: boolean }];
Here, DistribFoo<T> takes any union of T extends Json and distributes Foo<T> across it, so that DistribFoo<string | number> becomes Foo<string> | Foo<number>. This helps in case processFooArray() takes an unordered array type.
And processFooArray() is generic in A, an array type of Json elements. Then the arr value it accepts is a mapped array type. When you call processFooArray(arr) the compiler can infer the A type from arr, and use it to validate that each member of the array is actually a Foo<T> for some T.
Note that I've made the return type an array of boolean values of the same length as the input arr, under the assumption that the function will (somehow!) call each function and map each function to its output.
Let's test it:
const a: Foo<string> = (n) => n.endsWith("a")
const b: Foo<number> = (n) => n < 0.5
const c = (x: HTMLElement) => x.hasAttribute("src")
processFooArray([a, b]); // okay
processFooArray([a, b, c]); // error!
// ------------------> ~
Looks good. The compiler is happy with [a, b], but unhappy with [a, b, c].
This approach probably also has its limits, especially if the library functions produce a value of "Foo<A> for some A"; maybe you can figure out what A should be based on the input to the library function, or maybe you just pick a random A, or never, or something. I won't go farther down this road though.
Playground link to code
anyor{}since you won't be able to determine what type a member function accepts apart from aconstarray. Maybe you could explain how you intend to use this?b['a'].bar(123)and this should work, because type signatureRecord<string, MyType<Json>>says me thatb['a']is of typeMyType<Json>, consequentlyb['a'].baris of type(n: Json) => boolean, so I should be fine passing a number to it, but in fact it only expects a string. This is the problem that you need to bypass, not that typescript is arguingprocessMyTypeArray()generic the normal way. Let me know which of these versions of things you want to see written up, preferably by editing the code in the question to motivate a solution. A toy example, but not one so "minimal" as to be useless.