Home > front end >  TypeScript: 'string' is assignable to the constraint of type T, but T could be instantiate
TypeScript: 'string' is assignable to the constraint of type T, but T could be instantiate

Time:10-25

I'd like to define a function that takes either a string or an array of strings and does something with them, e.g. uppercasing them.

If the function takes a string, it should return a string. And, if it takes an array of strings, it should return an array of strings.

Here is my failed attempt:

const uppercase = <Params extends string | string[]>(params: Params): Params => {
  if (typeof params === "string") {
    return params.toUpperCase(); // ERROR: 'string' is assignable to the constraint of type 'Params', but 'Params' could be instantiated with a different subtype of constraint 'string | string[]'.
  }

  return params.map(param => param.toUpperCase()); // ERROR: 'string[]' is assignable to the constraint of type 'Params', but 'Params' could be instantiated with a different subtype of constraint 'string | string[]'
}

How would you fix this?

CodePudding user response:

If you will allow casting, for me the errors went away when I added casting on return values

const uppercase = <Params extends string | string[]>(params: Params): Params => {
    if (typeof params === "string") {
      return params.toUpperCase() as Params; 
    }
  
    return params.map(param => param.toUpperCase()) as Params; 
  }

Edit

You check the type with an if. Which allows you to know for sure: the object return is the same type as the argument. But TS doesn't know that. That [string returned implies string received].

I think you can accomplish similar to what you want with class generics.

const uppercase = <Type extends string|string[]>(params: Type, converter: UpperCaser<Type>) => {
    return converter.toUpperCase(params);
}

abstract class UpperCaser<Param> {
    abstract toUpperCase(param: Param): Param;
}

class StringArray extends UpperCaser<string[]> {
    toUpperCase(stringArray: string[]): string[] {
        return stringArray.map(param => param.toUpperCase());
    }
}

class StringObj extends UpperCaser<string> {
    toUpperCase(string: string): string {
        return string.toUpperCase();
    }
}

It doesn't need to be precisely this approach; adjust to your liking.

CodePudding user response:

In order to do it, you should overload your function:

const toUppercase = (str: string) => str.toUpperCase();

function uppercase(params: string[]): string[]
function uppercase(params: string): string
function uppercase<Params extends string | string[]>(params: Params) {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)

}

const str = uppercase('hello') // string
const arr = uppercase(['hello']) // string[]

Playground

If you are interested in more sophisticated typings, consider this example:

const toUppercase = <Str extends string>(str: Str) => str.toUpperCase() as Uppercase<Str>;

type TupleUppercase<
  T extends string[],
  Accumulator extends string[] = []
  > =
  (T extends []
    ? Accumulator
    : (T extends [infer Head, ...infer Tail]
      ? (Head extends string
        ? (Tail extends string[]
          ? TupleUppercase<Tail, [...Accumulator, Uppercase<Head>]>
          : never)
        : never)
      : never)
  )


function uppercase<Param extends string, Params extends Param[]>(params: [...Params]): TupleUppercase<Params>
function uppercase<Param extends string>(params: Param): Uppercase<Param>
function uppercase<Params extends string | string[]>(params: Params) {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)

}

const str = uppercase('hello') // "HELLO"
const arr = uppercase(['hello', 'world']) // ["HELLO", "WORLD"]

Playground

Please see the docs

P.S. You are not allowed to return generic Params in your implementation because it is considered unsafe behavior. See this answer and/or my article

Consider this UNSAFE example:

const toUppercase = (str: string) => str.toUpperCase();

function uppercase<Params extends string | string[]>(params: Params): Params {
  return typeof params === "string" ? toUppercase(params) : params.map(toUppercase)
}

uppercase('hello') // 'hello' but 'HELLO' in runtime

CodePudding user response:

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

CodePudding user response:

This people like to complicate them selfs. There are many ways of fix ing this but the easiest

if (typeof (param as string)==="string") {

or another one would be


if (Array.isArray(param)) {}

Instead of comparing if it is an string check if it is an array

  • Related