Let’s say I have a type to denote a language code, for example:
type LangCodeUpper = 'EN' | 'DE' | 'ES';
type LangCodeLower = Lowercase<LangCodeUpper>;
The problem I’m trying to solve is the following:
const lang: LangCodeUpper = 'EN';
const lowerLang = lang.toLowerCase(); //this is `string`, while I want `LangCodeLower`
So far, I’ve got:
type LangCodeUpperDecl = 'EN' | 'DE' | 'ES';
type LangCodeLowerDecl = Lowercase<LangCodeUpper>;
type LanguageCaseImpl<T> = Omit<T, 'toLowerCase' | 'toUpperCase'> & {
toUpperCase?: () => LangCodeUpperDecl;
toLowerCase?: () => LangCodeLowerDecl;
};
type LangCodeUpper = LanguageCaseImpl<LangCodeUpperDecl>;
type LangCodeLower = LanguageCaseImpl<LangCodeLowerDecl>;
Which kind of works, since this successfully assigns the correct type when calling one of the two methods. But it also introduces new problems, namely:
function a(lang: LangCodeUpper) {
return this.title[lang]; //TS2538: Type 'LangCodeUpper' cannot be used as an index type.
}
The obvious solution is to introduce some kind of wrapper object, that would hold the value and provide methods like toCodeUpper/toCodeLower. Yet I would much rather stick with a basic type – is there a better way to override the methods for these types?
>Solution :
As you have noticed String#toLowerCase() and String#toLowerCase() both return arbitrary strings and don’t use the built in utility types Lowercase and Uppercase. There is an open GitHub issue (microsoft/TypeScript#44268) which addresses this behavior but I don’t think we can expect any news on it very soon.
As of now, you can extend the global String interface with a custom overload.
interface String {
toLowerCase<T extends string>(this: T): Lowercase<T>;
}
type LangCodeUpper = 'EN' | 'DE' | 'ES';
type LangCodeLower = Lowercase<LangCodeUpper>;
const lang: LangCodeUpper = 'EN';
const lowerLang = lang.toLowerCase();
// ^? const lowerLang: "en"