Talk about the best practices of TypeScript type declaration

jafeney
中文

头图

TypeScript has been born for a long time, and everyone knows its advantages and disadvantages. It can be said to be a powerful tool for JavaScript static type checking and syntax enhancement. For better code readability and maintainability, we have accepted TypeScript in every old project. Reconstructed fate. However, in the process of transformation, I gradually realized the artistic charm of TypeScript language

There are not many ruthless people, let's first talk about the skills related to TypeScript type declaration:

First understand TypeScript's type system

TypeScript is a superset of JavaScript, it provides all the functions of JavaScript, and an additional layer on top of these functions: TypeScript's type system

图片

What is TypeScript's type system? For a simple example, JavaScript provides basic data types such as String, Number, Boolean, etc., but it does not check whether the variable correctly matches these types. This is also a natural defect of the JavaScript weak type verification language. There may be people here. Those advantages of DIS weakly typed languages. But it is undeniable that many large-scale projects have planted numerous bugs due to this weak type of implicit conversion and some imprecise judgment conditions. Of course, this is not the subject of our discussion today.

Unlike JavaScript, TypeScript can detect in real time whether the types of variables in our written code are matched correctly. With this mechanism, we can detect unexpected behaviors that may occur in the code in advance when writing the code, thereby reducing the chance of error. The type system consists of the following modules:

Derivation type

First of all, TypeScript can automatically generate types based on the variables declared by JavaScript (this method can only be used for basic data types), such as:

const helloWorld = 'Hello World'  // 此时helloWorld的类型自动推导为string

Definition type

Furthermore, if some complex data structures are declared, the function of automatically deriving types appears inaccurate. At this time, we need to manually define the interface:

const helloWorld = { first: 'Hello', last: 'World' } // 此时helloWorld的类型自动推导为object,无法约束对象内部的数据类型

// 通过自定义类型来约束
interface IHelloWorld {
  first: string
  last: string
}
const helloWorld: IHelloWorld = { first: 'Hello', last: 'World' }

Union type

You can create complex types by combining simple types. Using union types, we can declare that a type can be a combination of one of many types, such as:

type IWeather = 'sunny' | 'cloudy' | 'snowy'

Generic

Generics is a rather obscure concept, but it is very important. Unlike joint types, generics are more flexible in use and can provide variables for types. Give a common example:

type myArray = Array // 没有泛型约束的数组可以包含任何类型

// 通过泛型约束的数组只能包含指定的类型
type StringArray = Array<string> // 字符串数组
type NumberArray = Array<number> // 数字数组
type ObjectWithNameArray = Array<{ name: string }> // 自定义对象的数组

In addition to the above simple use, you can also dynamically set the type by declaring variables, such as:

interface Backpack<T> {
  add: (obj: T) => void
  get: () => T
}
declare const backpack: Backpack<string>
console.log(backpack.get()) // 打印出 “string”

Structure type system

One of the core principles of TypeScript is that the focus of type checking is on the structure of values, sometimes called "duck typing" or "structured typing". That is, if two objects have the same data structure, they are treated as the same type, such as:

interface Point {
  x: number
  y: number
}

interface Rect {
  x: number
  y: number
  width: number
  height: number
}

function logPoint(p: Point) {
  console.log(p)
}
const point: Point = { x: 1, y: 2 }
const rect: Rect = { x:3, y: 3, width: 30, height: 50 }

logPoint(point) // 类型检查通过
logPoint(rect) // 类型检查也通过,因为Rect具有Point相同的结构,从感官上说就是React继承了Point的结构

In addition, if the object or class has all the required properties, TypeScript will consider them to match successfully, regardless of the implementation details

Distinguish the difference between type and interface

Both interface and type can be used to declare TypeScript types, which is easy for novices to make mistakes. Let's briefly list the differences between the two first:

Comparison itemtypeinterface
Type combinationCan only be merged by &Automatically merge with the same name, expand by extends
Supported data structureAll typesCan only express object/class/function types
Note: Since interface supports automatic merging of types with the same name, when we develop some components or tool libraries, we should use interface declarations as much as possible for the types of input and output parameters, so that developers can make custom extensions when calling.

In terms of usage scenarios, the purpose of type is more powerful, not limited to expressing object/class/function, but can also declare basic type aliases, union types, tuples and other types:

// 声明基本数据类型别名
type NewString = string

// 声明联合类型
interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
type SmallPet = Bird | Fish

// 声明元组
type SmallPetList = [Bird, Fish]

3 important principles

TypeScript type declarations are very flexible, which means that one thousand Shakespeare can write one thousand Hamlet. In team collaboration, for better maintainability, we should practice the following three principles as much as possible:

Generic is better than union type

Give an official sample code for comparison:

interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
// 获得小宠物,这里认为不能够下蛋的宠物是小宠物。现实中的逻辑有点牵强,只是举个例子。
function getSmallPet(...animals: Array<Fish | Bird>): Fish | Bird {
  for (const animal of animals) {
    if (!animal.layEggs())
      return animal
  }
  return animals[0]
}

let pet = getSmallPet()
pet.layEggs() // okay 因为layEggs是Fish | Bird 共有的方法
pet.swim() // errors 因为swim是Fish的方法,而这里可能不存在

There are three problems with this naming method:

  • First, the type definition makes getSmallPet become limited. From the code logic point of view, its function is to return an animal that does not lay eggs, and the returned type points to Fish or Bird. But what if I just want to pick out a bird that does not lay an egg from a group of birds? By calling this method, I can only get a magical creature that may be Fish or Bird.
  • Second, the code is duplicated and difficult to expand. For example, if I want to add another turtle, I have to find all places similar to Fish | Bird, and then modify it to Fish | Bird | Turtle
  • Third, type signatures cannot provide logical correlation. Let’s examine the type signature again, and we can’t see why Fish | Bird is here instead of other animals. What is the relationship between the two of them and logic before they can be placed here.

Between the above problems, we can use generics to refactor the above code to solve these problems:

// 将共有的layEggs抽象到Eggable接口
interface Eggable {
  layEggs(): boolean
}

interface Bird extends Eggable {
  fly(): void
}
  
interface Fish extends Eggable {
  swim(): void
}
  
function getSmallPet<T extends Eggable>(...animals: Array<T>): T {
  for (const animal of animals) {
    if (!animal.layEggs()) return animal
  }
  return animals[0]
}
  
let pet = getSmallPet<Fish>()
pet.layEggs()
pet.swim()

Ingenious use of typeof derivation is better than custom types

This technique can be used in code without side effects, the most common is the constant data structure defined by the front end. For a simple case, when we use Redux, we often need to set the initial value for the State of each module of Redux. In this place, you can use typeof to deduce the data structure type of the module:

// 声明模块的初始state
const userInitState = {
  name: '',
  workid: '',
  avator: '',
  department: '',
}

// 根据初始state推导出当前模块的数据结构
export type IUserStateMode = typeof userInitState // 导出的数据类型可以在其他地方使用

This technique allows us to be "lazy" very calmly, and it can also reduce some of the type declarations in Redux, which is more practical

Skillful use of built-in utility functions is better than repeated declarations

The built-in tool functions provided by Typescript are as follows:

Built-in functionuseexample
Partial<T>All subsets of type T (each attribute is optional)Partial<IUserStateMode>
Readony<T>Return the same type as T, but all attributes are read-onlyReadony<IUserStateMode>
Required<T>Return the same type as T, each attribute is requiredRequired<IUserStateMode>
Pick<T, K extends keyof T>Partial attribute K selected from type T`Pick<IUserStateMode, 'name''workid''avator'>`
Exclude<T, U extends keyof T>Remove some attributes U from type T`Exclude<IUserStateMode, 'name''department'>`
NonNullable<T>Remove null and undefined from property TNonNullable<IUserStateMode>
ReturnType<T>Return the return value type of the function type TReturnType<IUserStateMode>
Record<K, T>Produce a type collection with attribute K and type TRecord<keyof IUserStateMode, string>
Omit<T, K>Ignore the K attribute in TOmit<IUserStateMode, 'name'>

The above tool functions, especially Partial, Pick, Exclude, Omit, Record, are very practical. You can do some deliberate exercises during the writing process.

Reference

阅读 4.9k

成功的道路并不拥挤,因为坚持的人不会太多

905 声望
884 粉丝
0 条评论
你知道吗?

成功的道路并不拥挤,因为坚持的人不会太多

905 声望
884 粉丝
宣传栏