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.