The question in the title pretty much says it all. The catch is that T
cannot be restricted.
Here is what I have tried:
class ArrayWrapper<T> {
constructor(private arr: T[]) {}
toMap<K, V>() {
return new Map(<T extends [K, V] ? [K, V][] : never>this.arr);
}
}
I have also tried this:
class ArrayWrapper<T> {
constructor(private arr: T[]) {}
toMap() {
return new Map(<T extends [infer K, infer V] ? [K, V][] : never>this.arr);
}
}
In both of the above cases the compiler is giving me a "Conversion of type 'T[]' to ... may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first" error.
So I tried something different and instead of never, I changed the false branch of the conditional type to never[]
. And this got rid of the compilation error but now when I do something like:
class ArrayWrapper<T> {
constructor(private arr: T[]) {}
toMap() {
return new Map(<T extends [infer K, infer V] ? [K, V][] : never[]>this.arr);
}
}
const notConvertibleToMap = [1, 2, 3];
const convertibleToMap: [number, string][] = [[1, 'one'], [2, 'two'], [3, 'three']];
const arr1 = new ArrayWrapper(notConvertibleToMap);
const map1 = arr1.toMap();
const arr2 = new ArrayWrapper(convertibleToMap);
const map2 = arr2.toMap();
The compiler infers Map<unknown, unknown> for both map1
and map2
.
What I would like is for the compiler to:
map1
, when trying to call toMap()
when T
is not [K,V]
either give an error or at the very least maybe infer a return type of never
since this function call is going to throw an error because T
is not a tuple and the Map constructor is going to throw.map2
, where T
IS a [K, V]
, I want the compiler to correctly infer a type of Map<K, V>
for map2
.Like I said at the beginning, obviously restricting the type of T
in the class like this
class ArrayWrapper<T extends [K, V], K, V> {
constructor(private arr: T[]) {}
toMap() {
return new Map(this.arr);
}
}
would allow for proper inference of K
and V
, but I don't want to restrict T. I need this class to accept arrays of any type of contained value T
.
Additional attempt was made with this:
class ArrayWrapper<T> {
constructor(private arr: T[]) {}
toMap() {
if (this.isArrayOfTuple(this.arr)) {
return new Map(this.arr);
}
throw new Error("called toMap() on ArrayWrapper that is not an array of tuples");
}
isArrayOfTuple<K, V>(arr: T[]): arr is [K, V][] {
return arr[0] instanceof Array && arr[0].length === 2;
}
}
but unfortunately the compiler is complaining over the return type of isArrayOfTuple
, saying that
A type predicate's type must be assignable to its parameter's type.
Type '[K, V][]' is not assignable to type 'T[]'.
Type '[K, V]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to '[K, V]'
If you want the compiler to make calling toMap()
an error if T
isn't assignable to [K, V]
for some K
and V
, then in some sense it doesn't matter what the output type is in such a case. It could be Map<unknown, unknown>
or Map<never, never>
or anything, as long as the toMap()
call is a compiler error. I think you'll end up with a runtime error (you can wade through the spec if you really care) so the function won't return... the "actual" return type is never
which can be safely widened to Map<unknown, unknown>
or anything you want without causing a type safety issue.
Anyway, to make the compiler error happen, you can give toMap()
a this
parameter which requires this
be of ArrayWrapper<[any, any]>
or something equivalent. You could use conditional type inference to manually infer K
and V
from T
:
toMap(this: ArrayWrapper<[any, any]>) {
type K = T extends [infer K, any] ? K : never;
type V = T extends [any, infer V] ? V : never;
return new Map<K, V>(this.arr);
}
but it's even easier to make toMap()
a generic method and have K
and V
inferred automatically:
toMap<K, V>(this: ArrayWrapper<[K, V]>) {
return new Map<K, V>(this.arr);
}
Either method will give you the behavior you're looking for:
const arr1 = new ArrayWrapper(notConvertibleToMap);
const map1 = arr1.toMap(); // <-- compiler error
// --------> ~~~~
// The 'this' context of type 'ArrayWrapper<number>' is not assignable to method's
// 'this' of type 'ArrayWrapper<[unknown, unknown]>'.
const arr2 = new ArrayWrapper(convertibleToMap);
const map2 = arr2.toMap(); // okay
// const map2: Map<number, string>