I have to change a specific strings defined as 'GET' | 'POST' | 'PATCH' | 'DELETE' to lowercase, without losing information about type. When I do just method.toLowerCase(), it changes its type back to string. I have to achieve something like 'get' | 'post' | 'patch' | 'delete'. I was able to do this using Generics and Type assertion, like this:
type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
export function request(method: Method) {
innerRequest(toLowerCase(method))
}
function innerRequest(method: Lowercase<Method>) {
console.log(method)
}
function toLowerCase<S extends string>(text: S) {
return text.toLocaleLowerCase() as Lowercase<S>;
}
I would like to know if there is a better way to achieve this, without using Type assertion(as Lowercase<S>)? Or is it a good approach to use it like this?
>Solution :
This does not work out of the box. But you can achieve this by extending the type of toLocaleLowerCase() to be a generic:
type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
declare global {
interface String {
toLocaleLowerCase<T extends string>(this: T): Lowercase<T>;
}
}
export function request(method: Method) {
innerRequest(toLowerCase(method))
}
function innerRequest(method: Lowercase<Method>) {
console.log(method)
}
function toLowerCase<S extends string>(text: S) : Lowercase<S> {
return text.toLocaleLowerCase();
}
What we are doing here is augmenting the type of String with a minor patch. Module augmentation is generally used to extend the types of third party libraries, but can also be used for core libraries right.
So instead of returning a string from toLocaleLowerCase, we would be returning a narrowed type based on the generic.
More info:
There is a Github thread where this is discussed in lengths here. Basically, the rationale for not adding this in the core type is that the end users of TypeScript are unblocked by using something like this. This obviously has its cons, which is mentioned in the thread like (affecting all usages)