How to use String.prototype.toUpperCase with const assertions in TS?

Viewed 32

I'm trying get uppercase version of a value in TS:

const directions = ['asc', 'desc'] as const

type Directions = typeof directions[number]

const [asc, desc] = directions

const order: Record<string, Uppercase<Directions>> = {
  a: asc.toUpperCase(),  // <- I need the type of this value to be 'ASC'
  d: desc.toUpperCase(), // <- I need the type of this value to be 'DESC'
}

But because String.prototype.toUppercase returns string following error is thrown:

Type 'string' is not assignable to type '"ASC" | "DESC"'.

I found two solutions so far, but neither one of them are preferable:

1: use Type Assertion (as):

const order: Record<string, Uppercase<Directions>> = {
  a: asc.toUpperCase() as Uppercase<Directions>,
  d: desc.toUpperCase() as Uppercase<Directions>,
}

Con: Type safety is not guaranteed - order can be changed to something like

const order: Record<string, Uppercase<Directions>> = {
  a: ''.toUpperCase() as Uppercase<Directions>,
  d: ''.toUpperCase() as Uppercase<Directions>,
}

and TS won't complain.

2: create custom toUpperCase function with different signature

const toUpperCase = <T extends string>(input: T): Uppercase<T> => input.toUpperCase() as Uppercase<T>

const order: Record<string, Uppercase<Directions>> = {
    a: toUpperCase(asc),
    d: toUpperCase(desc),
}

Con: Developers have to be taught that they shouldn't use the built in function and it might confuse junior developers on whether they should use the built in method or the custom one.

Is there a type safe solution while using the native String.prototype.toUppercase method?

1 Answers

As you note, currently the type checker does not understand that, for example, "asc".toUpperCase() will produce a value of literal type "ASC". There is a suggestion at microsoft/TypeScript#44268 that the typings for the toUpperCase() and the toLowerCase() methods of string values be updated so that they return the appropiate intrinsic string manipulation types. Currently it's listed as "awaiting more feedback", so if you want to see this happen you might want to give it a . For now it's not part of the language.


Luckily, if you want to get this behavior now, you can use declaration merging to add appropriate call signatures to the String interface. It looks like this:

// declare global { 
    interface String {
        toUpperCase<T extends string>(this: T): Uppercase<T>;
        toLowerCase<T extends string>(this: T): Lowercase<T>;
    }
// }

(If you are writing your code inside a module then you need to use global augmentation via declare global in order to merge the call signatures into the global String interface and not a module-local interface of the same name.)

The call signatures use a this parameter of a generic type to access the string literal type of the string on which you call the methods. Otherwise you'd get Uppercase<string> and/or Lowercase<string> which isn't specific enough for you (in TS4.7 and below this is just string, while in TS4.8 and above these types are more specific than string and refer to "uppercase strings" and "lowercase strings", as implemented in microsoft/TypeScript#47050. But neither of these are what you want for "asc".toUpperCase()).


Let's test it out:

const order = {
    a: asc.toUpperCase(),
    d: desc.toUpperCase(),
}
/* const order: {
    a: "ASC";
    d: "DESC";
} */

const obj = {
    ASC: 123,
    DESC: "321"
}
obj[order.a].toFixed(2); // okay
obj[order.d].toLowerCase(); // okay

Looks good. The compiler knows that order.a is "ASC" and order.d is "DESC".

Playground link to code

Related