How to type a function (in typescript) so that the input "arrayness" preserves in the output.
If input is array, output is array too. If input is a single value, output is a single value too.
Now there's an error saying that the output type can be both an array or a single value, and one of the options doesn't match. However it's clear what that type should be. How can it be expressed?
TS Playground
interface Target {
src: string | string[];
dest: string;
}
const forwardSlash = (s: string) => s.replace(/\\/g, '/');
const fixTargetPathsForViteStaticCopyOnWindows = (targets: Target[]) => {
const transformListOrSingleValue = <T,>(x: T | T[], f: (a: T) => T): T | T[] => {
return Array.isArray(x) ? x.map(f) : f(x);
};
const fixSingleTarget = (target: Target): Target => {
return {
...target,
src: transformListOrSingleValue(target.src, forwardSlash),
dest: transformListOrSingleValue(target.dest, forwardSlash), //Type 'string | string[]' is not assignable to type 'string'.
};
};
return targets.map(fixSingleTarget);
};
The error:
Type 'string | string[]' is not assignable to type 'string'.
Type 'string[]' is not assignable to type 'string'.ts(2322)
index.d.ts(28, 5): The expected type comes from property 'dest' which is declared here on type 'Target
PS Although this variation resolves my problem,
const forwardSlash = (s: string) => s.replace(/\\/g, '/');
const transformTargetsToForwardSlashFormat = (targets: Target[]) => {
const transformListOrSingleValue = <T>(x: T | T[], f: (a: T) => T): T | T[] => {
return Array.isArray(x) ? x.map(f) : f(x);
};
const fixSingleTarget = (target: Target): Target => {
return {
...target,
src: transformListOrSingleValue(target.src, forwardSlash),
dest: forwardSlash(target.dest),
};
};
return targets.map(fixSingleTarget);
};
I still would like to know if it's doable in the initial way.
How to type a function (in typescript) so that the input "arrayness" preserves in the output.
If input is array, output is array too. If input is a single value, output is a single value too.
Now there's an error saying that the output type can be both an array or a single value, and one of the options doesn't match. However it's clear what that type should be. How can it be expressed?
TS Playground
interface Target {
src: string | string[];
dest: string;
}
const forwardSlash = (s: string) => s.replace(/\\/g, '/');
const fixTargetPathsForViteStaticCopyOnWindows = (targets: Target[]) => {
const transformListOrSingleValue = <T,>(x: T | T[], f: (a: T) => T): T | T[] => {
return Array.isArray(x) ? x.map(f) : f(x);
};
const fixSingleTarget = (target: Target): Target => {
return {
...target,
src: transformListOrSingleValue(target.src, forwardSlash),
dest: transformListOrSingleValue(target.dest, forwardSlash), //Type 'string | string[]' is not assignable to type 'string'.
};
};
return targets.map(fixSingleTarget);
};
The error:
Type 'string | string[]' is not assignable to type 'string'.
Type 'string[]' is not assignable to type 'string'.ts(2322)
index.d.ts(28, 5): The expected type comes from property 'dest' which is declared here on type 'Target
PS Although this variation resolves my problem,
const forwardSlash = (s: string) => s.replace(/\\/g, '/');
const transformTargetsToForwardSlashFormat = (targets: Target[]) => {
const transformListOrSingleValue = <T>(x: T | T[], f: (a: T) => T): T | T[] => {
return Array.isArray(x) ? x.map(f) : f(x);
};
const fixSingleTarget = (target: Target): Target => {
return {
...target,
src: transformListOrSingleValue(target.src, forwardSlash),
dest: forwardSlash(target.dest),
};
};
return targets.map(fixSingleTarget);
};
I still would like to know if it's doable in the initial way.
Share Improve this question edited Feb 3 at 15:50 danronmoon 3,8735 gold badges35 silver badges58 bronze badges asked Feb 1 at 23:18 yevtyevt 8162 gold badges8 silver badges23 bronze badges 8 | Show 3 more comments1 Answer
Reset to default 1If you want the return type of a function to depend on whether or not one of its inputs is an array, you will either need to use conditional types with generics, or you will need to use overloads to represent the different allowable ways to call the function. Note that all of this is more complex than just having separate code paths for arrays and non-arrays. Trying to put those two fundamentally distinct operations in one function involves type juggling.
Here's how you'd use generics with conditional types:
declare function transformListOrSingleValue<T>(
x: T,
f: (a: T extends readonly any[] ? T[number] : T) => T extends readonly any[] ? T[number] : T
): T extends readonly any[] ? T[number][] : T;
Here T
always corresponds to the type of the x
input. If it is an array, then the conditional types of the form T extends readonly any[] ?
Target
? Is that something we need for this to make sense and for us to see the same error you're seeing? If so, please provide it. If not, please replace with something else. – jcalz Commented Feb 1 at 23:54forwardSlash(target.dest)
as mentioned... but if you need to represent the "array-in-array-out" in the call signature it's going to be more complicated. TS can't analyze the body of the function to determine this, so you'll have to manually curate the call signature either as a generic with a lot of conditional types, or as a series of overloads covering all the possible cases of how it's called. This playground link shows both approaches. Does that fully address the q? If so I'll write an a; if not, what's missing? – jcalz Commented Feb 3 at 16:29T[number][]
how we got to the second dimension. Even if this is just a typo, why it still works if i remove []? Lines34-37: Why we need to cover all 3 cases: T, T[], T | T[] (i tried similar approach and it seemed like first two cases should be enough). Why lines 36 and 37 are duplicated? Also, how do we choose which variation of overload to use as a function declaration? Looking forward for you a's – yevt Commented Feb 3 at 23:04[]
, the output will always bestring
that way, neverstring[]
, which is apparently fine forsrc
and fine fordest
, but wouldn't be fine if you had an array-only type as shown here. Do you need the other questions answered before you can be sure if my suggestion fully addresses the question? Or can I just write up an actual answer which touches on these points. – jcalz Commented Feb 3 at 23:38