Problem
Let's take a look at the critical part of each error message:
'Params' could be instantiated with a different subtype of constraint 'string | string[]'
What is an example where Params could be instantiated with a different subtype of constraint string | string[]?
const foo = 'foo';
uppercase(foo);
– TypeScript Playground
If you hover over the declaration for the foo variable, you'll see it has the following type:
const foo: 'foo'
And then if you hover over the call to uppercase, you'll see it has the following type:
const uppercase: <"foo">(params: "foo") => "foo"
Type 'foo' extends string so it satisfies the generic constraint. However your function's return type is now 'foo' while toUpperCase returns string. While type 'foo' is type string, not all strings are 'foo's.
Similarly if you, say, pass a tuple:
const tuple: ['foo'] = ['foo'];
uppercase(tuple)
– TypeScript Playground
The type for uppercase will become:
const uppercase: <["foo"]>(params: ["foo"]) => ["foo"]
however map returns string[]. While type ['foo'] is type string[], not all string[]s are ['foo']s.
In both cases you don't want the constrained type.
(Partial) Solution
You can use function overloads to ensure the correct return type is paired with the correct argument type:
function uppercase(params: string): string;
function uppercase(params: string[]): string[];
function uppercase(params: string | string[]): string | string[] {
if (typeof params === "string") {
return params.toUpperCase();
}
return params.map(param => param.toUpperCase());
}
– TypeScript Playground
I understand that you want your function return implementation to be type checked as well (ie you don't want it to be possible to return an array when a string is the argument and vice versa) however I don't believe it is possible[reference], at least with overloads and without significant changes – I see there's an answer that uses classes. While the solution I've noted here does not do checks on the implementation, it does do checks on the call site, which is some value:
const uppercaseString = uppercase('foo');
const uppercaseArray = uppercase(['foo']);
// Attempt to call Array.prototype.map on a string
uppercaseString.map();
^^^
Property 'map' does not exist on type 'string'.
// Attempt to call String.prototype.substring on an array
uppercaseArray.substring();
^^^^^^^^^
Property 'substring' does not exist on type 'string[]'.
– TypeScript Playground