preface

For those of you who already know JavaScript, getting started with TypeScript is actually quite easy. You just need to understand the basic type system to make the transition from JS to TS.

// js
const double = (num) = > 2 * num

// ts
const double = (num: number) :number= > 2 * num
Copy the code

However, as applications get more complex, it’s easy to set some variables to type any, and TypeScript becomes AnyScript. To give you a deeper understanding of TypeScript’s type system, this article focuses on its advanced types to help you get rid of AnyScript.

The generic

Before we get into advanced types, we need to understand briefly what generics are.

Generics is an important concept in strongly typed languages. Properly using generics can improve code reusability and make the system more flexible. Here’s what Wikipedia says about generics:

Generics allow programmers to write code in strongly typed programming languages using types that are specified later and specified as parameters when instantiated.

Generics are represented by a pair of Angle brackets (<>), and the characters inside these brackets are called type variables, which are used to represent the type.

function copy<T> (arg: T) :T {
  if (typeof arg === 'object') {
    return JSON.parse(
      JSON.stringify(arg)
    )
  } else {
    return arg
  }
}
Copy the code

This type T, we don’t know until we call copy, we only know what type T stands for when we call copy.

const str = copy<string> ('my name is typescript')
Copy the code

We can see from VS Code that the arguments and return values of copy already have types. That is, when we call copy, we assign a string to T. In fact, we can omit the Angle brackets when we call copy, and we can determine that T is string by type derivation of TS.

High-level types

In addition to basic types like String, number, and Boolean, we should also look at some advanced uses of type declarations.

Cross type (&)

The simple point of cross type is to combine multiple types into one type. Personally, I think it is more reasonable to call it “merge type”. Its grammatical rules and logical “and” symbol are consistent.

T & U
Copy the code

Let’s say I have two classes, a button and a hyperlink, and NOW I need a button with a hyperlink, I can use a cross type to do that.

interface Button {
  type: string
  text: string
}

interface Link {
  alt: string
  href: string
}

const linkBtn: Button & Link = {
  type: 'danger'.text: 'Jump to Baidu'.alt: 'Jump to Baidu'.href: 'http://www.baidu.com'
}
Copy the code

The joint type (|)

The syntactic rules for a union type are the same as the symbol for the logical “or”, indicating that its type is any one of multiple connected types.

T | U
Copy the code

For example, in the previous Button component, our Type property could only specify a fixed number of strings.

interface Button {
  type: 'default' | 'primary' | 'danger'
  text: string
}

const btn: Button = {
  type: 'primary'.text: 'button'
}
Copy the code

Type Alias (Type)

If you need to use the previously mentioned cross and union types in more than one place, you need to declare an alias for the two types in the form of a type alias. Type aliasing is similar to the syntax for declaring variables, replacing const and let with the type keyword.

type Alias = T | U
Copy the code
type InnerType = 'default' | 'primary' | 'danger'

interface Button {
  type: InnerType
  text: string
}

interface Alert {
  type: ButtonType
  text: string
}
Copy the code

Type index (KEYOF)

Keyof is similar to Object.keys and is used to obtain the associative type of the Key in an interface.

interface Button {
    type: string
    text: string
}

type ButtonKeys = keyof Button
/ / equivalent
type ButtonKeys = "type" | "text"
Copy the code

Taking the Button class as an example, the type of Button comes from another class ButtonTypes. According to the previous writing, every time ButtonTypes is updated, the Button class needs to be modified. If we use keyof, we will not have this trouble.

interface ButtonStyle {
    color: string
    background: string
}
interface ButtonTypes {
    default: ButtonStyle
    primary: ButtonStyle
    danger: ButtonStyle
}
interface Button {
    type: 'default' | 'primary' | 'danger'
    text: string
}

// With keyof, ButtonTypes is changed, and the type type is automatically changed
interface Button {
    type: keyof ButtonTypes
    text: string
}

Copy the code

Type constraint (extends)

The extends keyword here is different from the inheritance of extends after class. The main purpose of using extends within generics is to constrain generics. Let’s use the copy method we wrote earlier for another example:

type BaseType = string | number | boolean

// This is the copy argument
// Can only be a string, number, or Boolean
function copy<T extends BaseType> (arg: T) :T {
  return arg
}
Copy the code

There’s a problem if we pass in an object.

Extends is often used with keyof. For example, if we have a method to get the value of an object, but the object is uncertain, we can use extends and keyof to constrain it.

function getValue<T.K extends keyof T> (obj: T, key: K) {
  return obj[key]
}

const obj = { a: 1 }
const a = getValue(obj, 'a')
Copy the code

The getValue method here can constrain the value of the key based on the parameter obj passed in.

Type mapping (in)

The function of the in keyword is mainly to do type mapping, traversing the key of the existing interface or traversing the joint type. The following uses the built-in generic interface Readonly as an example.

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

interface Obj {
  a: string
  b: string
}

type ReadOnlyObj = Readonly<Obj>
Copy the code

We can structure under this logic, the first keyof Obj get a joint type ‘a’ | ‘b’.

interface Obj {
    a: string
    b: string
}

type ObjKeys = 'a' | 'b'

type ReadOnlyObj = {
    readonly [P in ObjKeys]: Obj[P];
}
Copy the code

Then P in ObjKeys equivalent to perform the logic of a forEach, traverse the ‘a’ | ‘b’

type ReadOnlyObj = {
    readonly a: Obj['a'];
    readonly b: Obj['b'];
}
Copy the code

You end up with a new interface.

interface ReadOnlyObj {
    readonly a: string;
    readonly b: string;
}
Copy the code

Condition types (U? X, Y)

Conditional types have the same syntax as ternary expressions and are often used in cases where the type is uncertain.

T extends U ? X : Y
Copy the code

So what that means is, if T is a subset of U, it’s type X, otherwise it’s type Y. Here’s an example using the built-in generic interface Extract.

type Extract<T, U> = T extends U ? T : never;
Copy the code

If the type in T exists in U, return, otherwise discard. Suppose we have two classes with three common attributes that can be extracted by using Extract.

interface Worker {
  name: string
  age: number
  email: string
  salary: number
}

interface Student {
  name: string
  age: number
  email: string
  grade: number
}


type CommonKeys = Extract<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'
Copy the code

Generic tools

TypesScript has many tool generics built into it, including Readonly and Extract. Built-in generics are defined in TypeScript’s lib.es5.d.ts, so they can be used without any dependencies. Here’s a look at some of the most commonly used tool generics.

Partial

type Partial<T> = {
    [P inkeyof T]? : T[P] }Copy the code

Partial is used to set all attributes of an interface to be optional by fetching T via keyof T, iterating through in, and finally adding a? .

When writing React components in TypeScript, we can set Partial values to optional values if the components have default values.

import React from 'react'

interface ButtonProps {
  type: 'button' | 'submit' | 'reset'
  text: string
  disabled: boolean
  onClick: () = > void
}

// Change the props properties of the button component to optional
const render = (props: Partial<ButtonProps> = {}) = > {
  const baseProps = {
    disabled: false.type: 'button'.text: 'Hello World'.onClick: () = >{},}constoptions = { ... baseProps, ... props }return (
    <button
      type={options.type}
      disabled={options.disabled}
      onClick={options.onClick}>
      {options.text}
    </button>)}Copy the code

Required

type Required<T> = {
    [P inkeyof T]-? : T[P] }Copy the code

Required is the opposite of Partial, which is to set all optional attributes in the interface to Required. Instead of -? .

Record

type Record<K extends keyof any, T> = {
    [P in K]: T
}
Copy the code

Record accepts two type variables, and the type generated by Record has properties that exist in type K and values of type T. One of the confusing points here is to add a type constraint, extends Keyof any, to type K, so let’s see what keyof any is.

K has been roughly type bound in string | number | symbol, just object type of index, namely type K can only be specified for this several types.

We often construct an array of objects in business code, but arrays are not easy to index, so we sometimes take a field of an object as an index and construct a new object. Suppose we have an array of commodity lists. To find the commodity named “daily nut” in the commodity list, we usually go through the array to find the commodity, which is tedious. For convenience, we will rewrite the array into an object.

interface Goods {
  id: string
  name: string
  price: string
  image: string
}

const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')

goodsList.forEach(goods= > {
  goodsMap[goods.name] = goods
})
Copy the code

Pick

type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}
Copy the code

Pick is used to extract certain attributes of an interface. If you have done Todo, you know that Todo only fills in the description information when editing, and only has the title and completion state when previewing. Therefore, we can use Pick to extract two properties of Todo interface and generate a new type TodoPreview.

interface Todo {
  title: string
  completed: boolean
  description: string
}

type TodoPreview = Pick<Todo, "title" | "completed">

const todo: TodoPreview = {
  title: 'Clean room'.completed: false
}
Copy the code

Exclude

type Exclude<T, U> = T extends U ? never : T
Copy the code

Exclude does the exact opposite of Extract. If the type in T does not exist in U, it is returned, otherwise it is discarded. Now let’s take the previous two classes and look at the result of Exclude.

interface Worker {
  name: string
  age: number
  email: string
  salary: number
}

interface Student {
  name: string
  age: number
  email: string
  grade: number
}


type ExcludeKeys = Exclude<keyof Worker, keyof Student>
// 'salary'
Copy the code

The salary of Worker that does not exist in the Student is extracted.

Omit

type Omit<T, K extends keyof any> = Pick<
  T, Exclude<keyof T, K>
>
Copy the code

Omit

and then construct a new type by Omit

. In the Todo example above, TodoPreview only needs to exclude the description attribute of the interface, which is a little more concise than the previous Pick.

interface Todo {
  title: string
  completed: boolean
  description: string
}

type TodoPreview = Omit<Todo, "description">

const todo: TodoPreview = {
  title: 'Clean room'.completed: false
}
Copy the code

conclusion

Getting used to TypeScript can be difficult if you only know the basics of TypeScript, and with the release of TypeScript 4.0 with more features, you have to keep learning and mastering it. I hope those of you reading this can learn something and get rid of AnyScript.