TypeScript has been around for a long time, and its advantages and disadvantages are well known. It is a powerful tool for static typing and syntax enhancement in JavaScript, and many of our older projects have embraced the fate of refactoring with TypeScript for better code readability and maintainability. However, in the process of transformation, I gradually realized the artistic charm of TypeScript

Without further ado, let’s talk about TypeScript type declarations:

Take a look at TypeScript’s type system

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

What is TypeScript’s type system? As a simple example, JavaScript provides basic data types such as String, Number, Boolean, etc., but it doesn’t check that variables match these types correctly, which is a natural flaw of JavaScript’s weak-type-checking language. There may be someone here who appreciates the benefits of weakly typed languages. Admittedly, there are numerous bugs in large projects due to weak-typed implicit conversions and lax criteria, which are not the subject of today’s discussion.

Unlike JavaScript, TypeScript detects in real time whether the types of variables in your code are correctly matched. This allows you to detect unexpected behavior in your code before it’s written, reducing the chance of errors. The type system consists of the following modules:

Derived types

First, TypeScript can automatically generate types based on variables declared by JavaScript (only for basic data types), such as:

Const HelloWorld = 'Hello World' // The type of HelloWorld is automatically deduced as String

Define the type of

Furthermore, if we declare complex data structures, the ability to automatically derive types is not accurate, so we need to manually define the interface:

const helloWorld = { first: 'Hello', last: // Constraint interface IHelloWorld {first: string last: // Constraint interface IHelloWorld {first: string last: // Constraint interface IHelloWorld {first: string last: string } const helloWorld: IHelloWorld = { first: 'Hello', last: 'World' }

The joint type

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

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

The generic

Generics are an obscure concept, but they are important because, unlike federated types, they can be used more flexibly and can provide variables for types. A common example:

Type myArray = Array // Arrays with a generic constraint can contain any type type type stringArray = Array<string> // string Array type NumberArray = Array < number > / / digital Array type ObjectWithNameArray = Array < {name: string} > / / custom arrays of objects

In addition to the simple use above, 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()) // Print out "string"

Structural type system

One of TypeScript’s core tenets is that type checking focuses on the structure of values, sometimes referred to as “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) // Type checking passes LogPoint (Rect) // Type checking also passes, because Rect has the same structure as Point. Senseally, React inherits the structure of Point

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

Distinguish between type and interface

Both interfaces and types can be used to declare TypeScript types, which can be confusing for beginners. Let’s briefly list the differences between the two:

Compare the item type interface
Type merge mode You can only merge with & The same name is automatically merged through the extends
Supported data structures All types of Only the Object/Class/Function types can be expressed

Note: Since Interface supports automatic merging of types of the same name, when we develop some components or libraries, we should use the Interface declaration whenever possible for the type that goes in and out of the arguments, so that developers can make custom extensions when calling

In terms of usage scenarios, type is more powerful than object/class/function. It can also declare basic type aliases, union types, tuples, and other types:

// declare union type interface Bird {fly(): void LayEggs (): void LayEggs (): boolean } interface Fish { swim(): void layEggs(): Boolean} type SmallPet = Bird | Fish / / declare tuples type SmallPetList = [Bird, Fish]

Three important principles

TypeScript type declarations are flexible, which means there are a thousand Hamlets for a thousand Shakespeares. In teamwork, for better maintainability, we should practice the following three principles as much as possible:

Generics take precedence over union types

For comparison, here is the official code example:

interface Bird { fly(): void layEggs(): boolean } interface Fish { swim(): void layEggs(): Boolean} // Get small pets. Pets that cannot lay eggs are small pets. The logic in the real world is a little bit far-fetched, just to give you an 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 pet. Swim () // errors Because swim is a Fish method, it may not exist here

There are three problems with this naming:

  • First, the type definition makesgetSmallPetBecome limited. From the logic of the code, what it does is return a non-egg-laying animal with a type pointing to Fish or Bird. But what if I just want to pick one bird out of a flock that doesn’t lay an egg? By calling this method, I can only get a magical creature that may be a Fish or a Bird.
  • Second, the code is repetitive and hard 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

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

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()

Dexterous use of typeof derivation is better than custom types

This technique can be used in code that has no side effects, most commonly with constant data structures defined in the front end. To take a simple case, when we use Redux, we often need to set the initial value of the State of each module in the Redux. Here we can use typeof to deduce the typeof the module’s data structure:

// declare the initial state const userInitState = {name: ", workId: ", avator: ", department: Export type IUserStateMode = typeof UserInitState // Export type IUserStateMode = typeof UserInitState // Export type IUserStateMode = typeof UserInitState // Export data type can be used elsewhere

This technique allows us to be very relaxed about being lazy, but also reduces the type declarations in Redux, which is useful

Smart 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 (each attribute is optional) Partial<IUserStateMode>
Readony<T> Returns the same type as T, but all properties are read-only Readony<IUserStateMode>
Required<T> Returns the same type as T. Each property is required Required<IUserStateMode>
Pick<T, K extends keyof T> Part of the attribute K selected from type T `Pick<IUserStateMode, ‘name’ ‘workid’ ‘avator’>`
Exclude<T, U extends keyof T> Removes part of the attribute U from type T `Exclude<IUserStateMode, ‘name’ ‘department’>`
NonNullable<T> Remove null and undefined from property T NonNullable<IUserStateMode>
ReturnType<T> Returns the return value type of function type T ReturnType<IUserStateMode>
Record<K, T> Produces a collection of types with attribute K and type T Record<keyof IUserStateMode, string>
Omit<T, K> Ignore the K attribute in T Omit<IUserStateMode, 'name'>

The above several tool functions, especially Partial, Pick, Exclude, Omit, Record, are very practical, which can be done deliberately in the preparation process

The resources