TypeScript recently introduced "template literal types", these basically allow us to use template string like semantics when creating literal types.
Along side this new functionality came some fairly useful new utility types: string manipulation types.
Template literal types
For example:
type Foo = 'foo';
type Bar = 'bar';
type FooBar = `${Foo | Bar}`; // "foo" | "bar"
As you can see, the new thing here is that we can interpolate other types within another string literal type.
This is pretty powerful stuff as it means we could infer strings based on other string-like types.
A few possible use cases:
-
${keyof T}-changed
to infer "foo-changed"-style events - Enumerating combinations of string literal types as above
- Inferring part of a string literal type
I won't go into this too much, but you can read more here.
String manipulation types
There are 4 new string manipulation types:
Uppercase<T>
Transforms a string literal type to uppercase:
type Foo = 'foo';
type UpperFoo = Uppercase<Foo>; // "FOO"
Lowercase<T>
Transforms a string literal type to lowercase:
type FooBar = 'FOO, BAR';
type LowerFooBar = Lowercase<FooBar>; // "foo, bar"
Capitalize<T>
Transforms a string literal type to have the first character capitalized:
type FooBar = 'foo bar';
type CapitalizedFooBar = Capitalize<FooBar>; // "Foo bar"
Uncapitalize<T>
Transforms a string literal type to have the first character lowercased:
type FooBar = 'Foo Bar';
type CapitalizedFooBar = Uncapitalize<FooBar>; // "foo Bar"
How?
If, like me, you wondered how these types can possibly work... the answer is compiler magic.
Usually TypeScript's utility types ultimately drill down to something you could have written yourself, for example:
// Record is defined as follows inside TypeScript
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// Before it existed, people were writing things like:
type Record = {[key: string]: any};
However, this time around, these new types are built into the compiler and cannot (easily) be written by us. As you can see:
// intrinsic is some special keyword the compiler
// understands, expected to never be used in userland code.
type Uppercase<S extends string> = intrinsic;
// under the hood, it uses this:
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
Examples
A few possible uses of this:
/*
* Technically you could use this to require UPPERCASE
* or lowercase...
*/
declare function foo<T extends string>(str: T extends Uppercase<T> ? T : never): void;
foo('ABCDEF'); // Works
foo('abcdef'); // Error
/*
* Or you might want a method to return a transformed
* version of a string...
*/
declare function toUpper<T extends string>(val: T): Uppercase<T>;
toUpper('foo' as string); // string
toUpper('foo'); // "FOO"
Wrap-up
If you want to know more about these types, see here:
https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
They're very cool and this new functionality opens up many doors. What was previously weakly typed as a string can probably now be strongly typed in many cases.
Give them a go!
Top comments (1)
Very cool and thanks for the write up. I now hereby challenge you to find a use for
intrinsic
in userland code 😉Thanks again