This presents a challenge when it comes to implementing generic functions with union type parameters. Check out microsoft/TypeScript#24085 for detailed information on this issue. The main problem lies in the fact that while the compiler can potentially narrow the type of arg.source
to, let's say, Int32Array
, it cannot narrow down the generic type parameter from T extends TypedArray
to something like T extends Int32Array
. It is generally unsafe to narrow down T
when you narrow a value of type T
, even though it would be quite convenient in scenarios like yours. As of now, this functionality is not supported.
To address this limitation and ensure your existing implementation compiles, you can utilize type assertions similar to what you have already done.
If I had nothing more to add, I would have left the other answer as-is. However, I wanted to demonstrate that by manipulating your implementation logic slightly, you can achieve some level of type safety without requiring an assertion:
const typedArrayMaker = (size: number) => ({
get Int32Array() { return new Int32Array(size); },
get Float32Array() { return new Float32Array(size); },
get Float64Array() { return new Float64Array(size); }
});
export function createTypedArray<K extends keyof ReturnType<typeof typedArrayMaker>>(
arg: { source: { [Symbol.toStringTag]: K }, arraySize: number }) {
return typedArrayMaker(arg.arraySize)[arg.source[Symbol.toStringTag]];
}
In this revised approach, we define a function named typedArrayMaker
which generates an object containing getter methods for different types of arrays. The compiler interprets the type of typedArrayMaker
as:
const typedArrayMaker: (size: number) => {
readonly Int32Array: Int32Array;
readonly Float32Array: Float32Array;
readonly Float64Array: Float64Array;
}
The createTypedArray()
function takes arguments as before, but the generic parameter is now represented by K
, corresponding to the value of the [Symbol.toStringTag]
property of arg.source
. Each typed array has a specific string literal value for this property, which is used for indexing into typedArrayMaker
.
This transformation results in a generic indexing operation for the return type, making it comprehensible by the compiler. The outcome is a return type dependent on K
, devoid of any errors. Let's put it to the test:
function test(i32: Int32Array, f32: Float32Array, f64: Float64Array, i8: Int8Array) {
const i32New = createTypedArray({ source: i32, arraySize: 128 }); // Int32Array
const f32New = createTypedArray({ source: f32, arraySize: 128 }); // Float32Array
const f64New = createTypedArray({ source: f64, arraySize: 128 }); // Float64Array
const i8New = createTypedArray({ source: i8, arraySize: 128 }); // error!
// ----------------------------> ~~~~~~
// Type '"Int8Array"' is not assignable to type
// '"Int32Array" | "Float32Array" | "Float64Array"'
}
As demonstrated, the compiler correctly identifies that i32New
, f32New
, and f64New
have the expected types, whereas i8
triggers an error when passed to createTypedArray()
due to the absence of that specific typed array type within our function.
This just goes to show one viable method of structuring the code in a manner that aligns with the compiler's understanding. In practical terms, I highly recommend resorting to a type assertion because complex higher-order functions involving getters and symbol properties can get convoluted.
I trust this explanation proves helpful; best of luck!
Link to playground for code samples