let's say we have a function toArray
that takes an argument and returns it as is if it is an array and if not returns an array with the argument value:
// function that takes a value and returns the value if it is of type array and if not returns the value in an array
export const toArray = <T>(value: T | T[]): T extends undefined ? [] : Array<T> => {
if (typeof value === "undefined") return [] as any;
return (Array.isArray(value) ? value : [value]) as any;
};
this function works fine as long as the type of the arguments is not union:
number => number[]
number[] => number[]
for unions I got not the expected result:
// wanted
'a'|'b' => ('a'|'b')[]
// currently
'a'|'b' => 'a'[] | 'b'[]
How do I tell typescript to infer array of union instead of union of arrays?
CodePudding user response:
As Typescript documentation says :
To avoid that behavior (distributivity), you can surround each side of the extends keyword with square brackets.
You should define the returned type of the function like this :
[T] extends [undefined] ? [] : Array<T>
CodePudding user response:
Note that TypeScript does not infer literal values from objects (including arrays). For that, you'll need to use a
const
assertion on object literal input values. (See examples in the code below.)
You can use generics with a function overload signature to achieve your goal.
Here's an example demonstrating the details in your question:
const intoArray: {
(value?: undefined): unknown[];
<T extends readonly unknown[]>(value: T): T[number][];
<T>(value: T): T[];
} = (value?: unknown) => typeof value === "undefined" ? []
: Array.isArray(value) ? [...value]
: [value];
const result1 = intoArray();
//^? const result1: unknown[]
const result2 = intoArray(undefined);
//^? const result2: unknown[]
const result3 = intoArray('hello');
//^? const result3: string[]
const result3Literal = intoArray('hello' as const);
//^? const result3Literal: "hello"[]
const result4 = intoArray(['hello']);
//^? const result4: string[]
const result4Literal = intoArray(['hello'] as const);
//^? const result4Literal: "hello"[]
const result5 = intoArray(['a', 'b']);
//^? const result5: string[]
const result5Literal = intoArray(['a', 'b'] as const);
//^? const result5Literal: ("a" | "b")[]
I used a function expression above because that's what you showed in the question, but it's more common to use a declaration with the overload pattern (you'll find this in the overwhelming majority of examples online):
function intoArray (value?: undefined): unknown[];
function intoArray <T extends readonly unknown[]>(value: T): T[number][];
function intoArray <T>(value: T): T[];
function intoArray (value?: unknown) {
return typeof value === "undefined" ? []
: Array.isArray(value) ? [...value]
: [value];
}