Preface:

With the development of front-end technology, the old technology can no longer give you a full b sense of loading. Who can master the cutting edge technology, who is the front end of the programmer, that the most beautiful boy. So the trend of front-end technology development has slowly begun to embrace TS. From the current frame, wheel, and future business code, TS will become more and more popular. The support for TS in VUE2 was not particularly good, so since vuE3, you started using TS. Some of the optimizations are not very good at this stage, but I believe that in the near future, these issues will not be an issue and will most likely run TS directly on future browsers. So WE may not use TS now, but it is a technology that we have to learn.

Why do we use TS

As users of a weakly typed language, ts may be a little uncomfortable at first. We are used to having one type suddenly become another type in our code, and we are used to inserting or deleting attributes into objects. But we’re not used to putting rules on variables, and we’re not used to the crazy feeling of the screen bursting with red when we first start using TS. But when you really get used to it and get used to it, it doesn’t exist. You’ll find that you can write code in blind, without having to write a line, refresh the browser, console.log(xx) a variable. Wait for you to finish writing a logical requirements, a refresh page, found that the function is normal, no error. It’s just two words. It smells good.

We might think that writing declarative files and being aware of data types would add a lot of work. This is true. In the long run, however, the benefits outweigh the disadvantages. It improves the reusability of the wheel and improves the readability, stability and maintainability of the code. Compared to the extra work, it was negligible.

I’m not going to make a big point here. From our real development perspective, as developers, we must have maintained old code from the past. Everyone’s specifications are not uniform, the ability is uneven, resulting in the code you see, all kinds of weird. As you iterate or modify, you get all kinds of weird bugs. Compared to this kind of mistake we are common?

The error is not terrible. What is terrible is that after the test is normal, because of various special reasons, the data structure changes, resulting in a certain situation can not access the variable error.

So what went wrong? This occurs when the type is not rigorous and the use of variables is not standardized.

The following code, presumably everyone has seen it?

const Obj = await ajax.get('xxx')
Obj.props1 = []
Obj.props2 = {}
delete Obj.data
Obj.getData()
/ * or * /
let isLoad = false
isLoad = 0
isLoad += '0'
isLoad = Number(isLoad)
if (isLoad) {
  isLoad = {}
} else {
  isLoad = []
}
Copy the code

We can arbitrarily change the type of the data, and if the logic of the code gets too complicated, and if there’s too many operations, it’s going to cause the type to be used differently than it’s declared. So there are some special cases where the variable is not accessible, or the function that’s supposed to be called becomes undefined and the object that’s supposed to be called becomes some other type and causes TypeError

So, using TS, in combination with ide tools, can avoid these problems in the first place. Next, I’m going to focus on the use of TS in actual project development from the perspective of project development. I won’t go into the basics of TS, so if you are not familiar with TS, it is recommended to read the basic documentation of TS in advance.

Use of TypeScript in project applications

1. Type declaration

In actual development, in addition to basic types such as number string Boolean, we will also use slightly more complex types such as object types, array types, function types, and special convention types

So let’s take a look at how these types are applied in real development.

  1. The object’s type is Interface

In the process of using JS development, object is a very widely used type. We can use interface to declare the type of an object. Like when we get the data from the background


interface DataType {
	id: number
	act_title: string
}
const Data: DataType = await ajax.get('/api/getData')
Copy the code

In this case, all the data that we’re getting from Ajax is any, because at compile time, TS doesn’t know what data we’re going to get from this interface, so I strongly recommend that you declare the data type that’s coming back in the background to the document. The data here is the source of all the data, only grasp here, then can lay the foundation for the development of the later. Now, of course, some people might say, well, what about the fact that in the background the data that’s returned is not fixed, like act_title is sometimes a string, sometimes a number, and the data that’s returned is sometimes an object, sometimes an array? I tell you what to do, you directly to the background development, do you know the meaning of the interface, do you know the type of interface need to be fixed? You know… And then kick him back and tell him to change the code. One criterion that must be followed is that the data type returned in the background is always fixed. As appropriate, in some cases, some attributes are missing. But once you have this property, then the type of the property has to be determined.

Interfaces are nested, such as:

interface DataType {
    id: number
    act_title: string
    theme: {
	color: string
        fontSize: number}}const Data: DataType = await ajax.get('/api/getData')
Copy the code

You could also write

interface ThemeType {
    color: string
    fontSize: number
}
interface DataType {
    id: number
    act_title: string
    theme: ThemeType 
}
const Data: DataType = await ajax.get('/api/getData')
Copy the code

The nice thing about writing this is that the ThemeType is a type that you can use if you need it later. The ThemeType above is the anonymous writing, and the following is the named writing. This is the same with js syntax. Of course, this is not to say that we can’t use the type of ThemeType directly by using the anonymous writing above. If we want to use the theme type and we don’t want to declare the theme Type, we can use ThemeType. This type is the same as the ThemeType declared in the following code block. But it certainly doesn’t look good. DataType[‘theme’] DataType[‘theme’] The answer is no. Use “.” It’s a way of writing a namespace, and I’ll talk about that later

2. Array generic, one of the Array types

Arrays are also a common type in development. We often use arrays and manipulate them when displaying lists.


const Data: ArrayThe < {id: number} > =await ajax.get('/api/getIds')  // We use array generics to declare an array with {id: number} for each item. The {id: number} is an anonymous interface. You can also declare an interface with its type name inside Angle brackets. As for generics, more on that later
// Or declare an array type using a method of type []

const Data: string[] =await ajax.get('/api/getStrings') // Data is an array of types in which each item is a string
Copy the code

When you declare a variable as an array, if you type ‘. ‘after it, the IDE will prompt you with all the array methods, such as Map, forEach, som and includes. These methods are very friendly to development. Even if you force it in, you will get an error. The same is true for objects. If you type ‘. ‘after a declared object, it will reveal all the declared properties in the object and we can embed interface in Array, and we can embed Array in interface, for example:



interface ListItem {
    id: number
    name: string
}
interface DataType {
    total: number
    list: Array<ListItem>
}
const Data: DataType = await ajax.get('/api/getData')
Copy the code

3. Tuples of array types

The Array generic type declares an Array of variable length and a fixed content type for each element of the Array. How do we declare a type if we use an array of fixed length with the same (or different) items? This is where the tuple type comes in. The format and contents of tuples are fixed, for example


type TupleType = [string.string.string.string.string.string.string]
const weekEcum: TupleType = ['Monday'.'Tuesday'.'Wednesday'.'Thursday'.'Friday'.'Saturday'.'Sunday']
weekEcum[0] / / Monday
weekEcum[7] The tuple has a fixed length, and the array does not have an eighth item
Copy the code

We can also declare a different tuple for each item


type TupleType = [number.string.boolean]
const shop: TupleType = [ 11250.'XXX Official Flagship Store'.true] // Store ID Specifies the name of the store
shop[0] // 返回的数据为 number
shop[1] // Return a string
shop[3] The tuple has a fixed length, and the array does not have a fourth item
Copy the code

Tuples are the convention that there can be different cases between items in an array. You can also limit the length of the array. We can control the types of variables more precisely in use. For example, the number of days per week, at most, is seven. When we declare the type, we limit the length so that we don’t have to cross the line during the development process.

4. Type of the function

In js, a Function is also a variable. If a Function is of any type, we can use the built-in Function to represent it. So when we declare the function, we still need to put it in the parameters, and return value limits, so as to avoid the call error and so on.



type GetDataFun  = (index: number) = > string
const getData: GetDataFun = (index: number) = > {
    const str = 'Happy everyday with typescript'
    return str[number]}Copy the code

As you can see from the above code, we declare a GetDataFun type, which is the type of a function that takes a number and returns a string. When we declare a function, we assign the right-hand side to the left-hand side. On the left-hand side of the equals sign is the type you declared, so if the body of the function on the right-hand side of the equals sign is different from the type you declared, you’re going to get an error


type GetDataFun  = (index: number) = > string
const getData: GetDataFun = (index: number) = > {  (index: number) => number cannot be assigned to (index: number) => string
    return index + 1
}
Copy the code

As you can see above, the type to the right of the equals sign actually takes a number argument and returns a number, so you get an error when you declare GetDataFun. As for the type of the body after the equals sign, how does it come up? This is the type derivation of TS. Even if you don’t declare a specific type for the body of the variable, ts will deduce the maximum possible type based on the body of the variable (for example, let a = 1; The default type for a is number, which you did not declare, but ts is derived from the value to the right of =). Take a look at the code below

type GetDataFun  = (index: number) = > [string.string.string]
const getData: GetDataFun = (index: number) = > { // An error will be reported because the return type of the function body (Array
      
       ) is not the same as the return type of the declared function [string, string, string].
      
    const classMember = [['Li Jiancheng'.'Li Shimin'.'Li Yuanji'], ['Li Zhi'.'Lee Seung-gan'.'Li Tai']] // The type derived by classMember is not a tuple. You want [[string, string, string], [string, string, string]], but it is actually Array
      
       >.
      
    return classMember[index]
}
Copy the code

As you can see from the top. The type that ts derives from the value is the most confortable type, not the more precise type, so we must be careful when using it. When we declare an anonymous function, how do we set its type


const getData = (index: number) :number= > {
return index + 1
}

const getData = function (index: number) :number {
    return index + 1
}

function getData (index: number) :number {
    return index + 1
}

const Obj = {
    getData: (index: number) :number= > {
	return index + 1}}const Obj = {
    getData: function  (index: number) :number {
	return index + 1}}const Obj = {
    getData (index: number) :number {
	return index + 1}}Copy the code

Function can set mandatory items and default values

const getData = (str: string) :number= > {
    return str.length
}
// If the parameter can not pass
constgetData = (str? :string) :number= > { // An error will be reported because undefined has no length and the parameter is added? Then STR is string or undefined which is, (STR? : string | undefined) = > number, then to undefined, STR has no length, so the complains.
    // ts considers all cases, so it avoids typeError cases
    return str.length
}
// We can write it like this, give it its default value
constgetData = (str? :string) :number= > {
    str = str || ' '  // STR is deduced to be a string when calling length, so no error is reported
    return str.length
}
// We could also write it this way
const getData = (str: string = ' ') :number= > { // If the input parameter is given a default value, this parameter will not use "?" This parameter is not required by default
    return str.length
}
Copy the code

5. Crossover types

Crossover typing is the merging of multiple types into a single type. This allows us to add existing types together to form a single type that contains all the required features of each type. It is commonly used for merging interfaces. For example:


interface A {
    name: string
    age: number
}
interface B {
    name: string
    gender: string
}

const a: A & B // A is of type {name: string; age: number; gender: string}
Copy the code

If the same item is of the same type, then the item type is the same as the name in the above code. If the item type is different, then the two types are merged. For example:

interface A {
    name: string
    age: number
    sayName: (name: string) = > void
}
interface B {
    name: string
    gender: string
    sayName: (gender: number) = > number
}

const a: A & B // A is of type {name: string; age: number; gender: string; SayName: (gender: number) => void && (name: string) => number
Copy the code

What does the crossover type of function represent? It’s a little hard to understand, but I’ll say it anyway. Like what

const fun = (gender: number) = > void && (name: number) = > number} // If the input parameters are exactly the same, then the type of fun is returned as void for an argument that receives a number, and the function returns the former
const fun = (gender: number) = > void && (name: string) = > number} // If the input arguments are different, then fun is of type, and the argument can take a number. If it takes a number, it returns void, which is the type that received the number. If it takes a string, it returns number, which is the type after &. This may be useful in some special situations
Copy the code

Crossover types for basic types, such as:

interface A {
    name: string
    age: number
}
interface B {
    name: number
    gender: string
}

const a: A & B // The type of a is {name: never; age: number; gender: string; } // Name is a string and then a number. There's no such thing as the intersection of the basic types string and number. Think about it, what kind of data is both a string and a number
Copy the code

Here we talk about never and by the way, never is a type that has no return value at all. Think about a function that returns never. Don’t return? The return value is of type void, which is the parent of null and undefined. If never is returned, there is only one case where the function must, or is likely to, throw an error during execution, such as:

const fun = (num: number) = > {
    if (num > 10) {
	thorw new Error()}return num + 1
} / / this function return values, will be calculated into the number | never
Copy the code

So since there are cross types, & the code above is certain of joint type, type 1 | type 2, we see the joint type

6. Union types

In development, it is often inevitable that a variable may be of this type or that type. For example, the background returns data to you, sometimes strings, sometimes numbers. Or your function needs to take a number or a string and process it into a specific type, such as:

interface DataType {
    id: number | string
}
const Data: DataType = await ajax.get('/api/getData')
/ / or
interface Style { width: string; height: string}
const creatStyle = (size: string | number) :Style= > {
    let style: Style
    if (isNaN(Number(size))) {
	style= {
            width: size.toString(),
            height: size.toString()
	}
    } else {
	style= {
            width: size + 'px'.height: size + 'px'}}return style
}
Copy the code

7. Literal types

We’ve already talked about the basic data types, and the types of the array object functions that are not the basic data types. These types can be subdivided, but the limits are very broad. Sometimes, we may need to limit a data to a very small range, so what should we do?

interface Info {
    name: string
    id: number
}

interface ShopEcum {
    [x: string]: Info 
}


const getShopInfo = (merchant_num: srting) = > {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009.name: 'XXX Official Flagship Store'}}return shopEcum[merchant_num]
}
getShopInfo('20000009')  // If the function is not 20000009, it will return undefined. If the return type is not Info, we may encounter problems during development
// So let's specify the type
interface Info {
    name: string
    id: number
}

type ShopStr = '20000009' // This type is a literal type. Not only string but also must be this value

type ShopEcum = {
    [x in ShopStr]: Info; // in for traversing ShopStr as key info as value, which will be said later
}

const getShopInfo = (merchant_num: ShopStr ) = > {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009.name: 'XXXXXX Official Flagship Store'}}return shopEcum[merchant_num]
}
getShopInfo('20000009') // An error will be reported if you pass in 20000009
Copy the code

Then one might ask, since 2000009 is dead, why do we need this type? That’s right. If a literal has only one value, we might as well just write it dead. But a literal type can be a combination of multiple types, which is what a literal type means

iinterface Info {
    name: string
    id: number
}

type ShopStr = '20000009' | '20000000' // We have expanded several stores on 20000009. Let's say I add 20 million

type ShopEcum = {
    [x in ShopStr]: Info;
}

const getShopInfo = (merchant_num: ShopStr ) = > {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009.name: 'XXX Official Flagship Store'
        },
	'20000000': {
            id: 20000000.name: 'JD shops'}}return shopEcum[merchant_num]
}
getShopInfo('20000009') // An error will be reported if you pass in something other than 20000009 or 20000000

// This is not the only application. We often use background returned states such as state

constThe state:number = await ajax.get('/api/getState')  // We have limited the type of the state, but the number is very broad. It is possible to use the number in a way that is out of bounds. For example, if the state is only 0, 1, 2, or 3, but you have determined that the state is 4.
// Let's be precise
type StateType = 0 | 1 | 2| 3
const state: StateType = await ajax.get('/api/getState') 

if (state === 4) { This condition will always return "false" because the types "StateType" and "4" do not overlap
    console.log(state)
}
Copy the code

8. Type calculation — Conversions between federated types and interfaces

Sometimes, our union type will be converted to the object’s type interface. A typical example is to use a key to access an object’s properties. If we do not restrict the value of the key, then any value of the key will appear, causing the accessed property to be empty. So if we want to restrict access to keys, we also have to declare a union class, which is the same as the keys of the object’s interface, which is cumbersome, so we use the keyword keyof


// Let's continue with the example above
interface Info {
    name: string
    id: number
}

interface ShopEcum {
    '20000009': Info 
    '20000000': Info 
}

/ / type ShopStr = '20000009' | '20000000' / / we write the joint type and the above ShopEcum keys are exactly the same, so why should write 2 times
We could write it this way
type ShopStr = keyof ShopEcum // Keyof means to take a key from an interface and form a joint strength. A bit like the js emphasis on the object.keys () method

const getShopInfo = (merchant_num: ShopStr ) = > {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009.name: 'XXX Official Flagship Store'
        },
	'20000000': {
            id: 20000000.name: 'xx shop'}}return shopEcum[merchant_num]
}
getShopInfo('20000009')
Copy the code

In addition, we can also use the keyword “in” to talk about the union type to interface


// Let's continue with the example above
interface Info {
    name: string
    id: number
}

interface ShopEcum {
    '20000009': Info 
    '20000000': Info 
}

type ShopStr = '20000009' | '20000000'

type ShopEcum =  {
    [x in ShopStr]: Info; // The in keyword iterates through ShopStr to make every item in ShopStr as a key value Info
}
// Remember that if we use in, then the interface declaration cannot use the interface keyword, otherwise an error will be reported, such as this
interface ShopEcum  { // This is wrong
    [x in ShopStr]: Info; // The in keyword iterates through ShopStr to make every item in ShopStr as a key value Info
}

const getShopInfo = (merchant_num: ShopStr ) = > {
    const shopEcum: ShopEcum = {
	'20000009': {
            id: 20000009.name: 'XXX Official Flagship Store'
        },
	'20000000': {
            id: 20000000.name: 'xx shop'}}return shopEcum[merchant_num]
}
getShopInfo('20000009')
Copy the code

9. Interface inheritance

We often use situations where an interface contains another interface, but these two are used separately, so how do we declare these two interfaces?

interface GiftType {
    id: number
    name: string
}

interface GiftTypeAndState {
    id: number
    name: string
    isUse: boolean
}

const isUse = [1.2.3]
const gift: Array<GiftType> = await ajax.get('/api/getGift')

const giftState: GiftTypeAndState  = gift.map((gift: GiftType) = > ({
    id: gift.id,
    name: gift.name,
    isUse: isUse.includes(gift.id)
}))

// We will find that GiftTypeAndState contains GiftType, so we can declare GiftTypeAndState using inheritance, i.e. extends
// This is similar to the crossover type, but the difference between inheritance is that if the current inherited interface has the same item as the inherited interface, the crossover type will make that type cross, while inheritance will replace the inherited attribute type with the current identical attribute type
interface GiftTypeAndState extends GiftType { 
    isUse: boolean
}
// For example, if
interface GiftTypeAndState extends GiftType { / / GiftTypeAndState id of type string, is no longer a number, but cross type id string | number is never
    id: string
    isUse: boolean
}
// If the interface inherits more than one thing
// Use commas (,) to separate items to be inherited
interface GiftTypeAndState extends GiftType, GiftType2 {
    id: string
    isUse: boolean
}

Copy the code

10. Generics

A generic type is an extension of a non-basic type. Used for reuse. If we think of the previous types as values, such as const val = value, then the generic type would represent const val = (parameter)=> value. That is, we take the form of a parameter and use the function call to get a given model type. It may not be easy to understand, but let’s look at an example:

// We request backend data to get the data structure like this
interface ListItem {
    id: number
    name: string
}
interface DataType {
    total: number
    list: Array<ListItem>
}
const Data: DataType = await ajax.get('/api/getData')

// When we call getData2, we have to declare a DataType and ListItem
interface ListItem2 {
    id: number
    title: string
}
interface DataType2 {
    total: number
    list: Array<ListItem>
}
const Data2: DataType2 = await ajax.get('/api/getData2') 
// In the DataType category, only the list item type is different. The rest, including the totle format and an array format, is different. Then we can declare a generic type that generates the DataType I want, passing ListItem as a parameter, so we only need to declare one ListItem each time we call the interface

type ListDataType<T> = {
    totalnum: number
    list: Array<T>
}
const Data: ListDataType<ListItem>= await ajax.get('/api/getData') 
const Data2: ListDataType<ListItem2>= await ajax.get('/api/getData2') 
// As you can see, we pass ListItem as a parameter to ListDataType and return a given structure T as a parameter


interface AnyObj = { // This interface represents any object type
    [x: string] :any
}

type ListDataType<T extends AnyObj> = { // In this case, T can only pass objects. If T is not an object, an error will be reported
    totalnum: number
    list: Array<T>
}
 // T extends AnyObj means that T must inherit from AnyObj, that is, T must be a subset of AnyObj or itself
// Give another example
type ListDataType<T extends number> = { // In this case, T can only pass objects. If T is not an object, an error will be reported
    totalnum: number
    list: Array<T>
}
const Data: ListDataType<1 | 2> =await ajax.get('/api/getData') / / because 1 | 2 is included in the correct number So is the number of subsets
const Data: ListDataType<number> =await ajax.get('/api/getData') // The correct number is itself
const Data: ListDataType<'2' | 1> =await ajax.get('/api/getData') // The union type is not exactly number

Copy the code

So T is going to be a parameter, whatever type we pass, and it’s going to end up in the list, but that’s not what we want. We just want him to put in one object. So can we limit the type? The answer is yes

interface AnyObj = { // This interface represents any object type
    [x: string] :any
}

type ListDataType<T extends AnyObj> = { // In this case, T can only pass objects. If T is not an object, an error will be reported
    totalnum: number
    list: Array<T>
}
 // T extends AnyObj means that T must inherit from AnyObj, that is, T must be a subset of AnyObj or itself
// Give another example
type ListDataType<T extends number> = { // In this case, T can only pass objects. If T is not an object, an error will be reported
    totalnum: number
    list: Array<T>
}
const Data: ListDataType<1 | 2> =await ajax.get('/api/getData') / / because 1 | 2 is included in the correct number So is the number of subsets
const Data: ListDataType<number> =await ajax.get('/api/getData') // The correct number is itself
const Data: ListDataType<'2' | 1> =await ajax.get('/api/getData') // The union type is not exactly number

Copy the code

Then T will be the parameter, which mimics the function’s parameter, and will have optional and default values, but note that if the generic parameter is not mandatory, default values must be set

// Generics cannot be used directly. You must pass parameters, but if the parameters in the generics are set to default values, they can be called directly


type ListDataType<T = string> = {
    totalnum: number
    list: Array<T>
}

interface ListItem {
    id: number
    title: string
}

const Data: ListDataType<ListItem>= await ajax.get('/api/getData') // List is ListItem
const Data: ListDataType = await ajax.get('/api/getData') // List is a string

// Note the generics of interface, which can also be written this way
interface ListDataType<T = string> {
    totalnum: number
    list: Array<T>
}
// Generics can take more than one parameter
interface ListDataType<T = string, P = number> {
    totalnum: number
    list: Array<T>
    list2: Array<P>
}

Copy the code

The above is a generic type for objects, but arrays and tuples are similar

type ListType<T> = ArrayThe < {id: number
    data: T
}>

type TupleType<T> = [T, T, T]

Copy the code

The generics of functions differ from the above in that generic arguments are not required by default. Let’s look at the usage scenario

// We divide function types into: function type declaration and anonymous function set types are left and right of the equal sign

type FunType  = <T>(val: T) = > { isEmpty: boolean; data: T }  // The function type is, we pass in an arbitrary value, and finally we generate an object containing an isEmpty property and data, which is the value passed in.
// We can see that this function doesn't care about the type of the argument, it just returns it after processing

// The following is the generic setting of the anonymous function declaration
const fun: FunType = <T>(val: T): { isEmpty: boolean; data: T } => {
    return {
        isEmpty:!!!!! val,data: val
    }
}
// In the TSX, write this so that no error is reported
constfun: FunType = <T extends any>(val: T): { isEmpty: boolean; data: T } => { return { isEmpty: !! Val, data: val}} // We can call this function without passing fun({a: T = {a: 1}; T = {a: 1}; Const data = ajax.get('/ API /getData') // data defaults to any fun<{a: Number}>(data) // The generic parameter T is {a: number}, not the derivation type of val any. Const Clone = <T extends AnyObj>(obj: T) const clone = <T extends AnyObj>(obj: T) T => { return JSON.parse(JSON.stringify(obj)) }Copy the code

After we look at generic functions, we’ll see that generic classes do the same thing


// We declare a DataCache class. This class is used to store the content that we put in and then retrieve it from the index
class DataCache<T> {
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

const obj = new DataCache()  Get (index: number)=> unkonwn (); // We don't pass generic arguments to new, so T defaults to unknown

// let's change this to a constructor
class DataCache<T> {
    constructor (defData: T) {
	this.set(defData)
    }
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

const obj = new DataCache(2)  // We don't pass in the generic arguments when we use new, but we pass in the construction arguments, so the generic class will derive the type of T from the type of the construction arguments, that is, you can see that the type of obj. Get is (index: number)=> number

// We can also pass arguments
const obj = new DataCache<number> ()Obj. get is of type (index: number)=> number

// The same is true for generic classes
class DataCache<T> {
    arr: Array<T> = []
    set (data: T) {
	this.arr.push(data)
    }
    get (index: number): T {
	return this.arr[index]
    }
}

class Cus extends DataCache<number> {
	getDouble (index: number) {
		return [ this.get(index), this.get(index + 1)]}}const a = new Cus()
a.getDouble(1) // a.getdouble of type (index: number)=> [number, number]

// A generic class can also inherit from a generic class and pass T to the inherited class
class Cus<T> extends DataCache<T> {
    getDouble (index: number): [ T, T ] {
        return [ this.get(index), this.get(index + 1)]}}const a = new Cus<number>()
a.getDouble(1) //  a.getDouble的类型为  (index: number)=> [ number, number ]


Copy the code

11. Type assertion

In our daily development, we may encounter a situation where you assign a data to a variable, but the data is not consistent with the variable to be assigned. In this case, TS will report an error saying that XXX cannot be assigned to XXX, XXX lacks the XX attribute in XXX, etc. So how do we deal with this situation?

// First, there is a problem in principle with assigning a value that is not of this type to a variable of this type. However, at times when we have to do this, TS also gives us access to type assertions

interface DataType {
    id: number
}
const data : DataType = {} / / complains
// We can assert that the value on the right-hand side of the equal sign is DataType. In this case, ts will assume that the type of {} is DataType, even though it is not.
const data: DataType = <DataType>{}
/ / or
const data: DataType = {} as DataType   // Note that the TSX can only have as because the Angle bracket form is mistaken for a tag

// There is another situation
const data: DataType = <DataType>{ name: 'wang' } // This will also report an error if you write it in the following form
const data: DataType = { name: 'wang' } as DataType  // The name attribute and the id attribute do not overlap, so this is wrong. What if we do this?

const data: DataType = { name: 'wang' } as unknown as DataType 
/ / or
const data: DataType = <DataType><unknown>{ name: 'wang' }

Copy the code

Having too many assertions in your code is very inelegant and has the potential to affect the logic and accuracy of your code, so I generally don’t recommend it. However, in some scenarios, we may have to use it in order to achieve business, such as the following scenarios:


/ / scenario 1
// We initialize a variable named data to accept an object. This object is a bunch of data. If you declare an interface, data will get the actual value in some way from the beginning, but if you initialize it with the default value, it would be very troublesome
interface DataType {
    val1: striing
    val2: number.valn: string
}
const data: DataType = {} as DataType

if (xxx) { // XXX must be true under the current condition, but ts code detection cannot deduce the case where it is true. Otherwise, we can simply declare const data: DataType without assigning it a value.
    data = await ajax.get('getData') // The data returned by Ajax is DataType
}

use(data)
// In some cases, react or vue will render the data once it is initialized. Ajax is asynchronous and will render the data once it is updated. This can lead to problems with incorrect data structures when initializing the render

/ / scenario 2
// The method claimed by the third party tool is inconsistent with the actual data. I met one in Taro.
// Taro is mainly aimed at wechat, so the above declaration does not declare the type of specific methods in Taobao mini programs. Some properties or methods are missing. Like the Canvas component
import { ComponentType } from 'react'
import { Canvas } from '@tarojs/components'
import { CanvasProps } from '@tarojs/components/types/Canvas'
// Canvas component in Ali must receive a width and height attribute, but taro Canvas component does not have the declaration of these attributes. If you impose them, an error will be reported

<canvas width={100} height={100} / >// ts will report an error, but in fact ali canvas has this property

// We need to add these two properties to the CanvasProps
interface MyCanvasProps extends CanvasProps { // We can't use interface merge to modify CanvasProps. We can only use a new interface to inherit CanvasProps
  width: number
  height: number
}
// Then we create a new variable to receive canvas
const MyCanvas: ComponentType<MyCanvasProps> = Canvas as unknown as ComponentType<MyCanvasProp>

<MyCanvaswidth={100} height={100} /> // This is ok
Copy the code

Again, assertions are not recommended because they can be lazy, but at the cost of breaking the stability of the code. Avoid using any and assertions unless you absolutely have to

12. Secure chained calls & forced chained calls

When we call a property of an object. We often get chained calls like A.B.C.D () and if we do, we have to make sure that when we call d. A, B, and C all exist, otherwise we need to make a judgment, such as a && a.b && A.B.C, but in TS we can use methods that are securely chained


/ / js
a && a.b && a.b.c && a.b.c()

/ / tsa? .b? .c()// will compile to the code above and return the same value as the code above

/ / it is important to note that if we need to get the return values of c, if use the chain calls, the return value of c will be undefined | expected value, because, if the parent didn't don't even have a b c, the expression of the return value is undefined

const a = {
    set: () = > { console.log(2); return 3333}}constb: { a? : {set: () = > number}} = {
    a
}
const x: number= b.a? .set()// Error will be reported because b.a? . The set () returns a value, not the set function is the expected return value of number, but the undefined | number


// Forced chained calls are different from secure chained calls. The former involves runtime, while the latter involves only compilation
// If we can guarantee that a property in an object must exist under certain conditions, but its type is optional. We can use methods that force chained calls so that ts does not report errors. (That is, we have to make sure that when we call here, we actually have this value. Kind of like an assertion)
const a = {
    set: () = > { console.log(2); return 3333}}constb: { a? : {set: () = > number}} = {
    a
}
const x: number= b.a! .set()// There is no error because ts thinks the code must call the set method

// If a does not exist,
const a = undefined
constb: { a? : {set: () = > number}} = { a } b.a! .set()// No error is reported at compile time, but errors are reported at run time

Copy the code

13. Type declaration files

Type declarations can be made in the executing code or in a declaration file. There are two types of declaration files. One is the global declaration file (declare the type in the file, do not need to import, can be used directly), and the other is the module declaration file (need to import to use).

Global declaration file:


// Put the TS files in any ts includes range
declare interface AnyObj{
  [x: string] :any
}


declare type state = 0 | 1 | 2;
Copy the code

Local declaration file:


// Put the TS files in any ts includes range
import { DataType } from "Typings"
export interface AnyObj{
    [x: string]: DataType 
}

export type state = 0 | 1 | 2;
Copy the code

As you can see, anything that has an inport or export is a local declaration file that needs to be imported. You can access the global type, but you cannot directly use the local type in the global type declaration file. This is used to prevent type contamination and is equivalent to creating our own separate type declaration file for the specified function

14. Namespaces

Namespaces are used to classify types. For example, if we put a lot of components or methods into a project, and the business project introduces the project, when we use the library, we sometimes use the type of the library’s functions, such as the input type of the function, the type of the component’s props. So we need to export the method or the component as well as the type. However, it would be confusing to export these types together, so we use namespace to classify them

// Method 1 fun1.ts
import { Fun1ArgType, Fun1ResType } from './type.d.ts';
export default (arg: Fun1ArgType): Fun1ResType= > {
    const res: Fun1ResType = doSomesting()
    return res
}

// Method 2 fun2.ts
import { Fun2ArgType, Fun2ResType } from './type.d.ts';
export default (arg: Fun2ArgType): Fun2ResType= > {
    const res: Fun2ResType = doSomesting()
    return res
}

// Type declaration file type.d.ts
export interface Fun1ArgType { xxx }
export interface Fun1ResType{ xxx }
export interface Fun2ArgType{ xxx }
export interface Fun2ResType { xxx }

// Export export
import fun1 from './fun1.ts';
import fun2 from './fun2.ts';

import { Fun1ArgType, Fun1ResType, Fun2ArgType, Fun2ResType } from './type.d.ts';

export {
    fun1,
    fun2,
    Fun1ArgType,
    Fun1ResType,
    Fun2ArgType,
    Fun2ResType
}
// You will find that there is no better classification of types than the name of the type, and it is difficult to know which function the type belongs to when using it

Copy the code

At this point we can use the namespace to solve the problem. Look at the code:


// Method 1 fun1.ts
import { Fun1ArgType, Fun1ResType } from './type.d.ts';
export default (arg: Fun1ArgType): Fun1ResType= > {
    const res: Fun1ResType = doSomesting()
    return res
}

// Method 2 fun2.ts
import { Fun2ArgType, Fun2ResType } from './type.d.ts';
export default (arg: Fun2ArgType): Fun2ResType= > {
    const res: Fun2ResType = doSomesting()
    return res
}

// Type declaration file type.d.ts
export namespace Fun1Types { 
    export interface ArgType{
        a: string
        b: number
    }
    export type ResType = 1 | 2 | 3
}
export namespace Fun2Types { 
    export interface ArgType{
   	name: string
    	id: number
    }
    export type ResType = Array<Fun2Types.ArgType> // It can be called recursively
}

// Export export
import fun1 from './fun1.ts';
import fun2 from './fun2.ts';

import { Fun1Types, Fun2Types } from './type.d.ts';

export {
    fun1,
    fun2,
    Fun1Types,
    Fun2Types
}
// In the caller, we can import the project, get Fun1Types, and use it, for example

import { fun1 , Fun1Types } from 'toos';

const res: Fun1Types.ResType = fun1({a: ' '.b: 1}) // We don't have to specify the type of the return value. Ts will automatically deduce the type of res based on the return value of fun1, but the reason for this is to make sure you know the type of the current value
Copy the code

15. Other

There are many deeper uses for TS, but in our business, they are almost impossible to use. Here are a few examples that might come in handy in case you need them

We need to declare a type that restricts an object to a fixed property, and other property values are optional, and can be any value

// We declare that an interface must have an ID, and any other value is optional
interface RequiredObj {
    id: number
    [x: string] :any
}

const A: RequiredObj = { id: 1 } / / right
const A: RequiredObj = { id: 1.a: 2 } / / right
const A: RequiredObj = { a: 2 } / / error


Copy the code

We need to declare a type that restricts an object to being a subset of another object, including itself


type Partial<T> = {
    [P inkeyof T]? : T[P] }interface DataType {
    id: number
    name: string
    age: number
}

const A: Partial<DataType> = { // Correct because the assigned objects have only one ID and are subsets of DataType
    id: 1
}


const A: Partial<DataType> = { // Error because the subset of DataType does not contain the X attribute
    id: 1.name:' '.x: 3
}
// In this application scenario, we write a function and a fixed object. The function also takes an object. The value of the object that takes a parameter must be a subset of that object
const Obj: DataType = { id: 1.name: 'wang'.age: 18 }
const upDateObj = (argObj: Partial<DataType>): DataType= > {
    return{... Obj, ... argObj } } upDateObj({id: 2}) / / right
upDateObj({id: 2.x: 3}) / / error
// Partial is a generic type that already exists
Copy the code

We often use promises, so how do we set the return value of async-decorated functions?


interface DataType {
    id: number
    name: string
    age: number
}


const getData = async (id: number) :Promise<DataType> => { // For async-decorated functions, the return value must be of type Promise, so if you want to set the type of the return value, you need to use the generic type Promise. The generic argument is the type of the value passed in to the function's resolve function
    return await ajax.get('/api/getData', { id })
}
/ / such as
const getData = async (id: number) :Promise<DataType> => {
    return new Promise((resolve: (res: DataType) => void) = > {
        resolve({
            id,
            name: 'wang'.age: 18})})}/ / such as
const getData = async (id: number) :Promise<DataType> => { // We return an object directly in the function, but because of the async modifier, the return value is still a promise
    return {
        id,
        name: 'wang'.age: 18}}Copy the code

About classes and their type declarations

// For classes, if you create a class, you will automatically generate an interface with the same name as the current class

/ / such as
class NumData {
    constructor (x: number) {
        this.x = x
    }
    x: number
    getData (y: number) {
        return this.x + y
    }
}
// In addition to having a NumData class that you can use for new, we also automatically generate an interface named NumData, which is automatically assigned to the result of new NumData(10)
const numData = new NumData(10) // The type numData is derived as numData

// You can also assign NumData to any object that matches this type, such as

const Obj: NumData = { // getData has a different function body, but the type of x and the type of getData are exactly the same as NumData
    x:1.getData: (y: number) = > {
        return y++
    }
}


Copy the code

Finally, all type declarations should be as big as possible, with a capital letter to easily distinguish the type from the variable. Otherwise, it is very troublesome to confuse it. These specifications are also available in ESLint.

That’s about it, so if you have anything to add, you can add it in the comments.

Author: Xu Chen