Yuck, yeah, I see that the default inference doesn’t work deeply enough to unroll Iterable<Iterable<T>>
into T
. It’s not that surprising if you look at how the typings for Iterable
are defined in the relevant library:
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
An Iterable<T>
has a symbol-keyed method whose return type is Iterator<T>
, which itself is defined as:
interface Iterator<T, TReturn = any, TNext = undefined> {
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}
where all the methods return IteratorResult<T, ...>
, which is defined to be a discriminated union type
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
whose members are
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
of which only one has a value
property of the relevant type T
you’re trying to find.
So to turn a type X
of Iterable<T>
into T
, the compiler needs to do something like Extract<ReturnType<ReturnType<X[typeof Symbol.iterator]>['next']>, { done?: false }>['value']
.
I think there’s probably some depth limit after which the compiler gives up trying to infer things. You can see that inferring T
from Iterable<T>
works (maybe about 5 or 6 layers of nesting), but inferring T
from Iterable<Iterable<T>>
is just too deep (10 or 12 layers?):
type N = number[][] extends Iterable<infer T> ? T : never; // number[] 👍
type O = number[][] extends Iterable<Iterable<infer T>> ? T : never; // unknown 👎
That leads me to the following workaround: make a type alias to explicitly operate on one layer of Iterable
, and then use that twice:
type DeIterable<T extends Iterable<any>> = T extends Iterable<infer U> ? U : never;
You can see this works:
type Okay = DeIterable<DeIterable<number[][]>>; // number 👍
And now flat()
can be defined like this:
function flat<II extends Iterable<Iterable<any>>>(
t: II
): Iterable<DeIterable<DeIterable<II>>> {
return [...t][0]
}
Where the input value is the generic type II
, and we use DeIterable
on it twice to get the T
you wanted before:
const flatted = flat(iterable) //return Iterable<number[]>
Looks good, now!