MWAN MOBILE

×
mwan_logo
blog-banner

7 Advanced Usages of TypeScript

Miscellaneous 12-Oct-2022

Are you coding a lot of redundant types? Here are some advanced features.

Whether you believe it or not, there are still have some senior developers who didn’t know about TypeScript. If you have no idea about what it is, do check out the Official Getting Started first.

This article is mainly for advanced usage for TypeScript.

Generics

// Bad
type APIResponse = {
  items?: any[];
  item?: any;
}

// Good
type APIResponse<T> = {
  items?: T[];
  item: T;
}

type User = {
  id: string;
}

const fetchCall = <T>(method: string, url: string): Promise<APIResponse<T>> => {
  // ...
}

export const getUser = (id: string): Promise<User> => {
  return fetchCall<User>(HttpMethod.GET, `/v1/user/${id}`)
    .then(({ item }) => item)
}

It is not a good practice to use anyavoid at all costs. The generic type is beneficial in most scenarios. Look at the snippet above. You can cast User to the response, which the type looks like:

{
item?: {
id: string
};
items?: {
id: string
}[];
}

Keyof Type Operator

type ExpiryDateTime = {
  days: number;
  hours: number;
  minutes: number;
}

const expiryDateTime: ExpiryDateTime = {
  days: 0,
  hours: 0,
  minutes: 0,
}

const onChange = (
  key: keyof ExpiryDateTime, // days | hours | minutes,
  val: number
): void => {
  expiryDateTime[key] = val
}

With keyof, you don’t have to use a union type for key in onChange. The expiryDateTime would also check the key whether it exists.

If you try to do something like this:

type Foo = { bar: number } & ExpiryDateTimeconst onChange = (
key: keyof Foo,
val: number
): void => {
expiryDateTime[key] = val
}

There will be a red line under expiryDateTime telling you that Property 'bar' does not exist on type 'ExpiryDateTime'. It will be the same error if you use string for key

keyof is helping you when refactoring. If you are using union type, then you need to do a double job.

Typeof Type Operator

typeof allows you to extract the type from a variable. Unlike JavaScript’s typeof TypeScript’s typeof allows to use in a type to refer it as a type, for example:

let foo = "bar";
let name: typeof foo; // string

I know this is too simple and redundant. Let’s try a complicated:https://betterprogramming.pub/media/fff4f2a47bb622a82ec29e71ae067d67

If we make changes on getHisOrHer return type, the type of HisOrHer will follow its changes, a one-time job.

Conditional Types

const getHisOrHer = (val: number): "his" | "her" => {
  // ...
}

type HisOrHer = ReturnType<typeof getHisOrHer> // "his" | "her"

// OR

type GetHisOrHerFunc = (val: number) => "his" | "her"

const getHisOrHer: GetHisOrHerFunc = (val) => {
  // ...
}

type HisOrHer = ReturnType<GetHisOrHerFunc> // "his" | "her"

Have you gotten into such an awkward scenario? Well, there is a solution for you with Conditional Types.https://betterprogramming.pub/media/6f9c6ba3e643b9ab684b1c04e46a0606

The concept is similar to JavaScript’s ternary operator condition ? true : false

s passed string as an argument so it will use the type StringId while n passed number and it will return NumberId

Unfortunately, you can’t have the third or more options.

Mapped Types

type Product = {
  [productName: string]: {
    amount: {
      selling: number;
      deno: number;
      cost: number;
    };
    code: string;
    subCode: string;
  }
}

In some cases, you will need a dynamic key for your type like the snippet above. You have the product name as the object key, but all the values are the same. With mapped types, you don’t have to create a type for each product name.

type RGB = {
  r: number;
  g: number;
  b: number;
}

type Color = {
  color1: RGB;
  color2: RGB;
}

type toSwitch<T> = {
  [Property in keyof T]: boolean;
}

const colorSwitch: toSwitch<Color> = {
  color1: false,
  color2: false,
}

Here goes the more complicated. Imagine you need two variables, one for the color value and another for the switches. This feature can reduce the chance of producing errors if you add more colors.

Mapping Modifiers

Prepare yourself. I will show you the more complicated examples with mapped types.

type RGB = {
  r: number;
  g: number;
  b: number;
};

type ImmutableColor = {
  readonly color1: RGB;
  readonly color2: RGB;
};

// Removes 'readonly' attributes from a type's properties
type toMutable<T> = {
  -readonly [Property in keyof T]: T[Property];
};
type MutableColor = toMutable<ImmutableColor>;

/*
type MutableColor = {
  color1: RGB;
  color2: RGB;
}
*/

You can use - as the prefix to remove the modifier or + to add a modifier. In the snippet, toMutable removed the readonly modifier.

Key Remapping via as

type RGB = {
  r: number;
  g: number;
  b: number;
};

type Color = {
  color1: RGB;
  color2: RGB;
};

type Getters<T> = {
  [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]
}

type ColorGetter = Getters<Color>;
/*
type ColorGetter = {
  getColor1: () => RGB;
  getColor2: () => RGB;
}
*/

Capitalize is a string manipulation for types, there are also a few more available:

  • Capitalize — Convert the first character to uppercase
  • Uncapitalize — Convert the first character to lowercase
  • Capitalize — Convert the first character to uppercase
  • Uppercase — Convert all characters to uppercase
  • Lowercase — Convert all characters to lowercase

Template Literal Types

type Locale = "zh" | "en" | "ms";
type Lang = `lang-${Locale}`;

// type Lang = "lang-zh" | "lang-en" | "lang-ms"

This feature is the same concept in JavaScript’s template literal. It builds the type on top of the string literal.

Utility Type

type TodoWithId = {
  readonly id: string;
  title: string;
  subtitle: null | string;
  description: string;
}

// Pick<T, Keys>
type Todo = Pick<TodoWithId, "title" | "subtitle" | "description">;
// Or
// Omit<T, Keys>
type Todo = Omit<TodoWithId, "id">

const t1: Todo = {
  title: "foo",
  subtitle: "bar",
  description: "",
};

// Partial<T>
const updateTodo = (todo: Todo, update: Partial<Todo>): Todo => {
  // ...
}
const t2 = updateTodo(t1, { description: "hello world" });

// Required<T>
const resetTodo = (todo: Todo, newTodo: Required<Todo>): Todo => {
  // ...
}
const t3 = resetTodo(t1, { description: "hello world" });
// Type '{ description: string; }' is missing the following properties from type 'Required<Todo>': title, subtitle

// Readonly<T>
const immutableTodo: Readonly<Todo> = t2;
immutableTodo.subtitle = ""
// TS2540: Cannot assign to 'subtitle' because it is a read-only property.

// Record<Key, T>
const obj: Record<string, Todo> = {
  t1: t1,
  t2: t2,
};

Last but not least, the utility types:

  • Pick<T, Keys> — Picking properties of key from the type
  • Omit<T, Keys> — The opposite of Pick , it takes all properties except Keys
  • Partial<T> — Similar to ? a.k.a optional , but you don’t have to make all the properties inside to be optional, because you might only need partial in a variable or function.
  • Required<T> — The opposite of Partial<T> , this will require all the properties include the optional one
  • Readonly<T> — The whole property would be immutable, and read-only
  • Record<Key, T> — I doesn’t recommend the use of object type in TypeScript, you will often get TS2339: Property 'x' does not exist on type 'object' . I always use Record<Key, T> as the replacement.

As you can see, most of the features can combine with each other to fully utilize it.

Whoever had the struggle as I had, I hope this article will simplify your work. This article doesn’t include all the advanced features; I picked the often used feature for example. If you want to know more, please check out the official documentation.

Bonus — Intellij based IDE

via GIPHY

When you are trying to migrate a JavaScript codebase to TypeScript codebase or refactoring a codebase that someone who loves to use any , this feature is your savior.

References

Source: Better Programming