avatarBytefer

Summary

The provided content is a comprehensive guide on utilizing TypeScript's template literal types to write more elegant and robust code, featuring examples and animations to aid understanding.

Abstract

The article "Using TypeScript Template Literal Types Like a Pro" is part of the "Mastering TypeScript" series, which aims to enhance TypeScript knowledge through animated explanations. It introduces the concept of template literal types, a feature introduced in TypeScript 4.1, and demonstrates how they can be used to create more maintainable and error-resistant code. The article covers the syntax and use cases of template literal types, including their ability to concatenate string literals and convert non-string primitive types to string literals. It also explains how to use built-in utility types like Uppercase, Lowercase, Capitalize, and Uncapitalize in conjunction with template literal types. The article further explores advanced topics such as type inference with conditional types and the infer keyword, as well as the use of the as clause for key remapping in mapped types. By the end of the article, readers are equipped with the knowledge to implement sophisticated utility types and are encouraged to follow the series for more insights into TypeScript.

Opinions

  • The author emphasizes the importance of TypeScript's template literal types for reducing code duplication and minimizing the risk of typos.
  • The article suggests that using template literal types in combination with TypeScript's utility types can lead to more readable and maintainable code.
  • The author believes that understanding TypeScript's advanced type features, such as conditional types and the infer keyword, is crucial for developers aiming to master the language.
  • The article promotes the idea that animations and visual examples can significantly enhance the learning process for complex TypeScript concepts.
  • By providing a series of articles on TypeScript, the author implies that continuous learning and practice are key to becoming proficient in TypeScript's advanced features.

Using TypeScript Template Literal Types Like a Pro

Write more elegant TypeScript code by learning how to use Template Literal Types. Explained with animations.

Welcome to the Mastering TypeScript series. This series will introduce the core knowledge and techniques of TypeScript in the form of animations. Let’s learn together! Previous articles are as follows:

Have you heard of TypeScript’s template literal types? Do you want to know how to use template literal types to write more elegant TypeScript code? If you want, after reading this article, maybe you will.

When developing web pages, we usually use Tooltip or Popover components to display some prompt messages or explanatory information. In order to meet different usage scenarios, Tooltip or Popover components will allow users to set their placement position. For example, top, bottom, left, right, etc.

Since string literal types can basically spell-check our string values, we define a Side type using TypeScript’s type aliases.

type Side = 'top' | 'right' | 'bottom' | 'left';
let side: Side = "rigth"; // Error
// Type '"rigth"' is not assignable to type 'Side'. 
// Did you mean '"right"'?ts(2820)

For the above 4 positions, it can already meet most scenarios. But if you want to set the placement position of the Tooltip more precisely, for example, let the Tooltip display in the upper left area of ​​the specified element:

Then the existing Side type can’t meet the requirements, so let’s define a new Placement type:

type Placement = Side
  | "left-start" | "left-end" 
  | "right-start" | "right-end" 
  | "top-start" | "top-end" 
  | "bottom-start" | "bottom-end"

In the Placement type, in addition to the original 4 positions, we add 8 new positions such as “left-start”, “left-end” and “right-start”, which correspond to the 8 string literal types. Looking at these string literal types, we find some duplicate codes “-start” and “-end”. In addition, when defining these string literal types, spelling errors may occur if you are not careful.

So how to solve the above problems better? This is where we can use the new template literal types introduced in TypeScript 4.1, which is used in the following way.

type Alignment = 'start' | 'end';
type Side = 'top' | 'right' | 'bottom' | 'left';
type AlignedPlacement = `${Side}-${Alignment}`;
type Placement = Side | AlignedPlacement;

After reading the above code, do you think it is much simpler? Similar to template strings in JavaScript, template literal types are enclosed in backticks and can contain placeholders of the form ${T}. The actual type of the type variable T can be string, number, boolean, or bigint.

Template literal types provide us with the ability to concatenate string literals and convert literals of non-string primitive types to their corresponding string literal types. Here are some examples:

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}-${S2}`;
type ToString<T extends string | number | boolean | bigint> = `${T}`;
type T0 = EventName<"foo">; // 'fooChanged'
type T1 = Concat<"Hello", "World">; // 'Hello-World'
type T2 = ToString<"bytefer" | 666 | true | -1234n>; 
// "bytefer" | "true" | "666" | "-1234"

For the above example, it’s not really that complicated. But now the question arises, what would be the result if the actual type of the EventName or Concat utility type passed in is a union type? Next, let’s verify that.

type T3 = EventName<"foo" | "bar" | "baz">; 
// "fooChanged" | "barChanged" | "bazChanged"
type T4 = Concat<"top" | "bottom", "left" | "right">;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

Why would such a type be generated? This is because, for template literal types, union types in placeholders are distributed over the template literal type.

`[${A|B|C}]` => `[${A}]` | `[${B}]` | `[${C}]`

And for cases with multiple type placeholders, such as the Concat utility type. Union types in multiple placeholders resolve to the cross product.

`${A|B}-${C|D}` => `${A}-${C}` | `${A}-${D}` 
  | `${B}-${C}` | `${B}-${D}`

After understanding the above operation rules, you should be able to understand the generated T3 and T4 types.

When working with template literal types, we can also use the built-in utility types provided by TypeScript for working with string types, such as Uppercase, Lowercase, Capitalize, and Uncapitalize, in the following way.

type GetterName<T extends string> = `get${Capitalize<T>}`;
type Cases<T extends string> = `${Uppercase<T>} ${Lowercase<T>} 
  ${Capitalize<T>} ${Uncapitalize<T>}`;
type T5 = GetterName<'name'>;  // "getName"
type T6 = Cases<'ts'>;  // "TS ts Ts ts"

In fact, the ability of template literal types is very powerful. Combined with TypeScript’s conditional types and the infer keyword, we can also implement type inference.

type InferSide<T> = T extends `${infer R}-${Alignment}` ? R : T;
type T7 = InferSide<"left-start">; // "left"
type T8 = InferSide<"right-end">; // "right"

In the above code, the InferSideutility type uses the TypeScript conditional types and infer in addition to the template literal types. If you are not familiar with this content, you can read Using TypeScript Conditional Types Like a Pro and Using TypeScript infer Like a Pro.

TypeScript 4.1, also allows us to use the as clause to remapping keys in mapped types. Its syntax is as follows:

where the type of NewKeyType must be a subtype of the string | number | symbol union type. In the process of remapping, combined with the capabilities provided by template literal types, we can implement some useful utility types.

For example, we can define a Getters utility type to generate corresponding Getter types for object types:

In the above code, since the type returned by keyof T may contain the symbol type, and the Capitalize utility type requires that the type to be processed needs to be a subtype of the string type, it needs to be type filtered by the & operator.

In addition to implementing simple utility types, we can also implement more complex utility types. For example, for getting the type of an object type with arbitrary hierarchical properties.

type PropType<T, Path extends string> = string extends Path
  ? unknown
  : Path extends keyof T
  ? T[Path]
  : Path extends `${infer K}.${infer R}`
  ? K extends keyof T
    ? PropType<T[K], R>
    : unknown
  : unknown;
declare function getPropValue<T, P extends string>(
  obj: T,
  path: P
): PropType<T, P>;

In the above code, the PropType utility type involves several core knowledge in TypeScript. In addition to recursive types, conditional types, conditional chains, and infer type inference are all covered in my previous articles.

I will cover it in a later article about recursive types. If you want to learn TypeScript, then don’t miss the Mastering TypeScript series.

Follow me on Medium or Twitter to read more about TS and JS!

Resources

More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

Typescript
JavaScript
Front End Development
Web Development
Programming
Recommended from ReadMedium