TypeScript has been around for a long time, and its strengths and weaknesses are well known. It’s a powerful tool for JavaScript static typing validation and syntax enhancement, and many of us have embraced TypeScript refactoring for better code readability and maintainability. However, in the process of transformation, I gradually realized the artistic charm of TypeScript

Let’s start with a few tips on TypeScript type declarations:

Look at TypeScript’s type system first

TypeScript is a superset of JavaScript that provides all the functionality of JavaScript with an additional layer on top of it: TypeScript’s type system

What TypeScript type system? To take a simple example, JavaScript provides basic data types like String, Number, and Boolean, but it doesn’t check that variables match those types correctly, which is an inherent flaw in JavaScript’s weak type verification language. There might be someone dissing those advantages of weakly typed languages. Admittedly, there are numerous bugs in large projects due to implicit conversions of weak types and lax judgment criteria, which is not the subject of today’s discussion.

Unlike JavaScript, TypeScript checks in real time to see if the types of variables in the code we write are correctly matched. This allows us to detect unexpected behavior as we write code, reducing the chance of errors. The type system consists of the following modules:

Derived types

First, TypeScript automatically generates types based on JavaScript declared variables (this only applies to primitive data types), such as:

const helloWorld = 'Hello World'  // The type of helloWorld is automatically inferred to string
Copy the code

Define the type of

In addition, if we declare some complex data structures, the automatic derivation of types is not accurate, so we need to define the interface manually:

const helloWorld = { first: 'Hello'.last: 'World' } // The type of helloWorld is automatically derived as object, and the data type inside the object cannot be constrained

// Constrain by custom type
interface IHelloWorld {
  first: string
  last: string
}
const helloWorld: IHelloWorld = { first: 'Hello'.last: 'World' }
Copy the code

The joint type

Complex types can be created by combining simple types. With union types, we can declare that a type can be a combination of one of many types, such as:

type IWeather = 'sunny' | 'cloudy' | 'snowy'
Copy the code

The generic

Generics are an obscure concept, but they are important because, unlike union types, they are more flexible and can provide variables for types. Here’s a common example:

type myArray = Array // Arrays without generic constraints can contain any type

// Arrays constrained by generics can contain only the specified type
type StringArray = Array<string> // Array of strings
type NumberArray = Array<number> // Array of numbers
type ObjectWithNameArray = ArrayThe < {name: string} >// An array of custom objects
Copy the code

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

interface Backpack<T> { add: (obj: T) => void get: () => T } declare const backpack: Backpack< String > console.log(Backpack.get())Copy the code

Structure type system

One of TypeScript’s core tenets is that type checking focuses 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, for example:

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) // Type check passed
logPoint(rect) // React also passes the type check, because Rect has the same structure as Point. React inherits Point structure
Copy the code

Additionally, iF an object or class has all the required attributes, TypeScript considers them a successful match, regardless of implementation details

Distinguish between type and interface

Interface and type can both be used to declare TypeScript types, which can be confusing for beginners. Let’s briefly outline the differences:

Compare the item type interface
Type merge mode You can only merge with & Automatic merge of the same name via extends
Supported data structures All types of Only object/class/function types can be expressed

Note: Since interface supports automatic merging of the same type, when we develop some components or libraries, we should use interface declaration as much as possible for the types of entry and exit parameters, so as to facilitate the developers to customize the extension when calling

Object /class/function can also be used to declare basic type aliases, associative types, tuples, etc.

// Declare the base datatype alias
type NewString = string

// Declare the union type
interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
type SmallPet = Bird | Fish

// Declare tuples
type SmallPetList = [Bird, Fish]
Copy the code

Three important principles

TypeScript type declarations are very flexible, which means a thousand Shakespeares can write a thousand Hamlets. For better maintainability in team collaboration, we should practice the following three principles as much as possible:

Generics are superior to union types

Here’s an official example code for comparison:

interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
// Get a pet, which is considered a pet that cannot lay eggs. The logic in reality is a little farfetched, just for example.
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 because layEggs is Fish | Bird common method
pet.swim() // errors because swim is a Fish method and may not exist here
Copy the code

There are three problems with this naming scheme:

  • First, type definitiongetSmallPetBecome limited. From the logic of the code, it returns an animal that does not lay eggs. The return type refers to Fish or Bird. But what if I just want to pick one bird in a flock that doesn’t lay eggs? By calling this method, I only get one magical creature that could be Fish or Bird.
  • Second, the code is repetitive and difficult to extend. I’d like to add a tortoise, for example, I have to find all similar Fish | Bird place, and then modify it into Fish | Bird | Turtle
  • Third, type signatures do not provide logical correlation. We will have a look at the type signatures, couldn’t see why there is a Fish | Bird rather than other animals, they are two exactly what is the relationship between logic and to be here

In view of the above problems, we can use generics to refactor the above code to solve these problems:

// Abstract the shared layEggs to the Eggable interface
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()
Copy the code

Clever Typeof derivation is better than custom typing

This technique can be used in code without side effects, most commonly with constant data structures defined in the front end. As a simple case, when we use Redux, we often need to set the initial value of State for each module of Redux. This is where you can derive the data structure typeof the module using typeof:

// Declare the initial state of the module
const userInitState = {
  name: ' '.workid: ' '.avator: ' '.department: ' ',}// Derive the data structure of the current module from the initial state
export type IUserStateMode = typeof userInitState // The exported data type can be used elsewhere

Copy the code

This technique allows us to be very comfortable with “lazy” and also reduces the type declarations in Redux, which is useful

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

Typescript provides several built-in utility functions:

Built-in function use example
Partial<T> All subsets of type T (optional for each attribute) Partial<IUserStateMode>
Readony<T> Returns the same type as T, but all properties are read-only Readony<IUserStateMode>
Required<T> Return the same type as T, and each attribute is required Required<IUserStateMode>
Pick<T, K extends keyof T> Partial attributes K selected from type T `Pick<IUserStateMode, ‘name’
Exclude<T, U extends keyof T> Remove some attributes U from type T `Exclude<IUserStateMode, ‘name’
NonNullable<T> Remove null and undefined from attribute T NonNullable<IUserStateMode>
ReturnType<T> Returns the return value type of function type T ReturnType<IUserStateMode>
Record<K, T> Produces a set of types with attributes K and types T Record<keyof IUserStateMode, string>
Omit<T, K> Ignore the K attribute in T Omit<IUserStateMode, 'name'>

The above tools, especially Partial, Pick, Exclude, Omit and Record, are very practical. You can do some deliberate practice in writing

The resources

  • TypeScript Series 5 Best Practices
  • Advanced Applications and Best Practices in TypeScript

This article is published by OpenWrite!