I have the following function that is erroring out on Array.from(list);
Type 'Element[]' is not assignable to type 'ArrayLike'
how to solve this and make the function work for both types ?
const reorderArray = (list: JSX.Element[] | number[], startIndex: number, endIndex: number): JSX.Element[] | number[] => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
TypeScript doesn't automatically distribute analysis over union types. So Array.from(x)
where x
is of type T[] | U[]
is not going to be T[] | U[]
; the best you could hope for would be a result of type (T | U)[]
, and that would require specifying the type argument explicitly (because the compiler doesn't like to synthesize new union types during inference):
const reorderArray = (
list: number[] | JSX.Element[], startIndex: number, endIndex: number
) => {
const result = Array.from<number | JSX.Element>(list);
// --------------------> ^^^^^^^^^^^^^^^^^^^^^^
// manually specify type argument
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
// return type is (number | JSX.Element)[]
Maybe that's acceptable to you.
If not and you want to distinguish the number[]
output from the JSX.Element[]
output then the recommended approach is use generics instead of a union:
const reorderArray = <T extends JSX.Element | number>(
list: T[], startIndex: number, endIndex: number
): T[] => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
Now the compiler understands all the operations in terms of the single generic type parameter T
. When you call the function with just number[]
or just JSX.Element[]
, the compiler can infer a type argument for T
which will produce an array of a single narrowed type:
const n = [1, 2, 3];
const o = reorderArray(n, 1, 2);
// const o: number[]
const p = [<div />, <p />, <h1 />]
const q = reorderArray(p, 1, 2);
// const q: JSX.Element[]
This still won't give you a union output for a union input:
const r = Math.random() < 0.5 ? n : p;
const s = reorderArray(r, 1, 2); // error, just like original Array.from error
const t = reorderArray<number | JSX.Element>(r, 1, 2);
// const t: (number | JSX.Element)[] // array of unions, not union of arrays
but if you plan to call reorderArray()
only on arrays of a known element type (and not a union of array types) then it doesn't matter.
If you need to have the output be a union of arrays, then things get hairier. Ultimately the language does not support correlated union types very well. See microsoft/TypeScript#30581. The approaches to deal with that are usually either to use a type assertion and move on:
const reorderArray2 = (
list: number[] | JSX.Element[], startIndex: number, endIndex: number
): number[] | JSX.Element[] => {
const result = Array.from<number | JSX.Element>(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result as number[] | JSX.Element[]; // <-- assert
};
or write redundant code to force the compiler to deal with cases:
const reorderArray3 = (
list: number[] | JSX.Element[], startIndex: number, endIndex: number
): number[] | JSX.Element[] => {
if (list.every((i): i is number => typeof i === "number")) {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
} else {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
}
};
or refactor to a particular flavor of generics as described in microsoft/TypeScript#47109 which is a lot of hard-to-explain hoop-jumping:
interface TypeMap {
n: number;
e: JSX.Element
}
type ArrayType<K extends keyof TypeMap> = { [P in K]: Array<TypeMap[P]> }[K]
const reorderArray4 = <K extends keyof TypeMap>(
list: ArrayType<K> & {}, startIndex: number, endIndex: number
): ArrayType<K> & {} => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const reorderArray5 = reorderArray4<keyof TypeMap>
// const reorderArray5: (
// list: number[] | JSX.Element[],
// startIndex: number,
// endIndex: number
// ) => number[] | JSX.Element[]
That last function is exactly what you asked for, but the procedure for getting the compiler to follow the logic is so tortured that I wouldn't recommend it unless your use cases require it.