“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

preface

Always wanted to write this article. But due to the lack of early preparation, has been tinkering, until now released. This article is some of my hands-on experience with typescript over the course of a year as I’ve come to understand and love the language. Like most people, I started with anyScript and slowly moved from roughing up types to using type inference and generics to being able to write some type utility functions myself. This article will detail the pitfalls I encountered and some of the practices of type-oriented programming.

This article doesn’t focus too much on the basics of typescript and requires an understanding of basic typescript syntax and the concept of generics. If you want to learn the basics, check out the typescript overview series I wrote earlier.

Based on article

This is a foundational article, but not the foundational part of typescript. On the contrary, these items may be used by beginners or relatively infrequently, so make sure you have a basic understanding of these items first. The following is just a hint or supplement to some of the items that are often used in daily development.

The keyof, in, and [] symbols operate on index types

The following three operators all operate on types that have indexes, and are mostly used only when type defines a type (except [] for index types).

First of all, an explanation of these three symbols is given:

  • [] is the index access operator. We can use the [] form equivalent to object writing alone or in conjunction with the keyof and in operators.

    interface Foo {
      a: number
      b: string
      c: boolean
    }
    // There will be a hint
    type FooKey = Foo['a'] // number
    Copy the code

    As simple as that, the [] operator lets us get the type of an index directly. It also allows us to pass in union types:

    interface Foo {
      a: number
      b: string
      c: boolean
    }
    // string | number | boolean
    type FooKey = Foo['a' | 'b' | 'c']
    // All types that match the above attributes can be federated
    Copy the code

    The result returned is also a combined type of all index value types. Work with other operators to read on.

  • Keyof is the index type query operator. Given that T is a type (which is typically an object type, or a type that can be written with interface), keyof T produces a type that is a combination of T’s property name string literal types. Here’s an example:

    interface Foo {
      a: number
      b: string
      c: boolean
    }
    // 'a' | 'b' | 'c'
    type FooKey = keyof Foo
    Copy the code

    Combined with the [] operator above:

    interface Foo {
      a: number
      b: string
      c: boolean
    }
    // string | number | boolean
    type FooKey = Foo[keyof Foo]
    Copy the code

    What do you say? Doesn’t that make it easier.

    Note: Although we can use it like this:

    type Bar = 'a' | 'b' | 'c'
    type BarKey = keyof Bar
    Copy the code

    Bar will be recognized as a string, and keyof will iterate over all the string properties and methods. This is not the answer we want. That’s why I emphasize that these operators are indexed and should be used by indexed objects or arrays.

  • In is the index traversal operator. Typescript complements typescript’s in operator on types. The IN operator allows us to iterate over each item of a union type and separate it into an index, just as we execute code:

    type Foo = 'a' | 'b' | 'c'
    
    /* type FooObj = { a: any; b: any; c: any; } * /
    type FooObj = {
      [K in Foo]: any
    }
    Copy the code

    As you can see, the IN operator generates a new object type from the iterated union type.

    This is interesting. With the previous keyof operator, we can combine them to map one type to another:

    // Change all indexes in T to readable type
    type Partial<T> = {
      [P inkeyof T]? : T[P] }Copy the code

In fact, many of typescript’s built-in utility types use these mapping-related operators, and many of our examples use these features as well, so keep them in mind.

Const type assertion

In typeScript 3.4, we introduced a new construct for literal values called const assertions. Its syntax is type assertion, const instead of the type name (e.g. 123 as const). When we construct new literal expressions using const assertions, we can signal to the language:

  • None of the literal types in this expression should be extended
  • Object text gets the readonly property
  • Array literals become readonly tuples
// Type '"hello"'
let x = 'hello' as const // The type is 'hello' and cannot be changed to string
// Const a = 'hello'

// As const is used on arrays or objects
// Type 'readonly [10, 20]'
const y = [10.20] as const

// Type '{ readonly text: "hello" }'
const z = { text: 'hello' } as const
Copy the code

Const assertions can help us reduce some of the more complex type writing, for example:

// Type: function Foo(): (number | (() => void))[]
function Foo() {
  let count = 0
  const setCount = () = > {
    count++
  }
  return [count, setCount]
}
Copy the code

The return value of the function is an array of associative type without as const. This is clearly not what we want, in which case we need to manually specify the return value of the function. Of course, it’s easier to add as const assertion

// Type: function Foo(): readonly [number, () => void]
function Foo() {
  let count = 0
  const setCount = () = > {
    count++
  }
  return [count, setCount] as const
}
Copy the code

This way, you get the correct tuple type when you call it.

Note:

  • constAssertions can only be applied immediately to simple literal expressions.
    // Error! A 'const' assertion can only be applied to a
    // to a string, number, boolean, array, or object literal.
    let a = (Math.random() < 0.5 ? 0 : 1) as const
    
    // Works!
    let b = Math.random() < 0.5 ? (0 as const) : (1 as const)
    Copy the code
  • constThe context does not immediately convert the expression to completely immutable (just shallow).
    const arr = [1.2.3.4]
    
    let foo = {
      name: 'foo'.contents: arr
    } as const
    
    foo.name = 'bar' // error!
    foo.contents = [] // error!
    
    foo.contents.push(5) / /... works!
    // Const arr = [1, 2, 3, 4] as const
    Copy the code

Interface declaration merge

We know that there are ways to define a typeinterfaceandtypeTwo,interfaceThere is a very special feature that can merge types as follows:

Therefore, the type defined using an interface can be changed later. However, a type defined using type does not have the type merge feature. After being defined, it cannot be modified again and can only be used as an alias.

Based on this characteristic, we may be able to explain the following question:

export interface IFoo {
  id: string
}
export interface IFoo2 {
  id: string
  [key: string] :string
}
type TFoo = {
  id: string
}

function foo(payload: Record<string.string>) {}

const iPayload: IFoo = {
  id: 'payload'
}

const iPayload2: IFoo2 = {
  id: 'payload'
}

const tPayload: TFoo = {
  id: 'payload'
}

/* Arguments of type "IFoo" cannot be assigned to arguments of type "Record
      
       ". Missing index signature in type "IFoo". * /
      ,>
foo(iPayload)
// Normal execution
foo(iPayload2)
// Normal execution
foo(tPayload)
Copy the code

In the above example, only the index type interface and the type defined by type can be passed to foo. Because of interface’s ability to merge types, there is no guarantee that IFoo’s type will change later, but after adding the index type, We specify that the value of the index type must be string, which matches the parameter type of the function no matter how it changes, thus passing typescript detection.

As mentioned above, when our foo function is defined as type:

function foo(payload: Record<string.any>) {}
Copy the code

The index value any is compatible with any types, so you don’t have to worry about merging the types of interfaces.

Basic usage of generics

Pay attention! Generics play a big role in typescript and are almost the most common feature we use every day, so I think it’s important to be familiar with generic parameters.

Generics can be declared when a class, function, or type is created, and we can dynamically limit the types of these structures by passing in their parameter values (we don’t do this manually in most usage scenarios, but use typescript’s auto-inference capabilities, which we’ll discuss in more detail).

Here is a brief list of the three corresponding writing formats:

  • Function:
    function foo<T> (bar: T) :T {
      return bar
    }
    
    foo<number> (1)
    Copy the code

    The call is passed in a generic parameter, limiting the type of the parameter that is passed in (it is also possible not to write number, which would manually restrict the type that is passed in foo to be number).

  • Class:
    // Define generic parameters here
    class Foo<T> {
      // borrow generic parameter constraint method types
      add(value: T): void{}}// Use generics to implement the internal algorithm for different variable types of a class, which is more efficient than any
    const foo1 = new Foo<number>() 
    foo1.add(1) // Only the type of numbwr can be entered
    
    const foo2 = new Foo<string>()
    min2.add('a') // Only string can be passed in
    Copy the code
  • Type:
    /* type Foo
            
              = { value: T keys: (keyof T)[] } */
            
    interface Foo<T> {
      value: T
      keys: (keyof T)[]
    }
    
    // Identify assignable types
    const foo: Foo<{ a: number; b: string} > = {value: {
        a: 1.b: '1'
      },
      keys: ['a'.'b']}Copy the code

Also, generics can be constrained using the extends keyword:

// T extends String means that the type of T extends from a substring, or that T is a subset of string
function foo<T extends string> (bar: T) :T {
    return bar
}

foo<number> (1) // error!
Copy the code

In this case, because we’re specifying that T must be a subset of string, any incoming number type will be an error.

Function overloading

For those of you who have used other languages like Java and C++, function overloading in typescript is not at compile time overload. Because it needs to be interoperable with javascript, it can’t generate multiple functions, so it’s just function signature overloading, and we’ll have to make our own judgments about the relevant conditions inside the actual function body.

Here’s a simple way to use it:

// The first two are overloaded signatures, the last one is the real function implementation, the first two are the real function type, and the last implementation function parameter and return value type must be compatible with all of the above signatures
function fun(name: string) :string
function fun(age: number) :string
function fun(payload: string | number) :string {
  if (typeof payload === 'string') {
    return 'name: '+payload
  } else {
    return 'age: ' + payload
  }
}
// Fun can pass only string and number arguments. If you pass other arguments, an error will be reported
console.log(fun('foo'))
console.log(fun(18))
console.log(fun(true)) // error
Copy the code

Usage scenarios

Function overloading is used when the function has multiple fixed arguments or return values, such as when we want to implement a function that supports both callback functions and promises:

function add(a: number, b: number, cb: (res: number) = >void) :void
function add(a: number, b: number) :Promise<number>
function add(
  a: number,
  b: number, cb? : (res:number) = >void
) :Promise<number> | void {
  const res = a + b
  if (cb) {
    cb(res)}else {
    return Promise.resolve(res)
  }
}

// The following two methods are indicated
add(1.2.(res) = > console.log(res))
add(1.2).then((res) = > console.log(res))
// You can only choose one or the other
add(1.2.(res) = > console.log(res)).then((res) = > console.log(res))
Copy the code

The use of condition types

Conditional types in typescript rely on the extends keyword, which is similar to a ternary expression in the following format:

type Type<T> = T extends U ? X : Y
Copy the code

Return X if T is a subclass of U, Y otherwise.

More specifically, you can do this:

type TypeName<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends undefined
  ? undefined
  : T extends() = >void
  ? () = > void
  : object
type Type = TypeName<() = > void> // () => void
type Type = TypeName<string[] >// object
Copy the code

Since there is no if statement, we usually use this nesting of ternary expressions for deep judgment.

Distributed condition type

A distributed condition type is a subset of the condition type. It is also called a distributed condition type if the type to be examined in the condition type is naked Type parameter. Distributed condition types are automatically distributed to union types when instantiated.

For example, instantiate T extends U? X, Y, T type | A | B C, will be resolved as (A extends U? X : Y) | (B extends U ? X : Y) | (C extends U ? X, Y).

Naked type parameters: other types of parameters, such as (string | number) [] is the other types of the parameters of the package, and string | number, it is not.

Take our example from above:

type TypeName<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends undefined
  ? undefined
  : T extends() = >void
  ? () = > void
  : object
type Type = TypeName<(() = > void) | string[] >// () => void | object
Copy the code

As you can see, our union type is parsed separately to form a new union type.

The use of infer

The infer operator must be used with a conditional type (with the extends keyword). Infer is used for type inference and assignment, usually followed by a generic variable whose return type is given to a subsequent generic variable that can be used to parse some aspect of it. This operator is often important when writing type tools. Here’s an example:

Infer can infer the types of elements in the array and assign values to U
type Type<T> = T extends Array<infer U> ? U : T 
// Check that the type is array
type Test = Type<string[] >// string
// Check that the type is not an array
type Test2 = Type<string> // string
Copy the code

The basic usage method is simple, but we can infer a lot of types from the original type, as can be seen in the following sections.

Using built-in types

Typescript provides us with a number of useful built-in typing tools that we can easily use to get the type we want.

As for how to use it. For example, I would use Record

in my project to create an object type to replace any with an index type constraint. Use the form Required

[‘ XXX ‘] to convert all values of an index Type to Required and get the Required definition Type (directly defining variable types with existing types), etc.

,>

interfaceButtonProps { onClick? :(e: MouseEvent) = > void
}
const foo: Record<string.any> = {}
const bar: Required<ButtonProps>['onClick'] = (e) = > {
    console.log(e)
}
Copy the code

For those of you interested, take a look at the utility types repository, which has some very versatile extension utility functions.

For index types

The main types of indexes are Partial, Required, Readonly, Pick, Omit, and Record.

// For the index type
/** * Make all properties in T optional */
// make all attributes of T optional
type Partial<T> = {
    [P inkeyof T]? : T[P };/** * Make all properties in T required */
// Make all attributes of T mandatory
type Required<T> = {
    [P inkeyof T]-? : T[P]; };/** * Make all properties in T readonly */
// Make all attributes of T read only
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

/** * From T, pick a set of properties whose keys are in the union K */
// Select K from T;
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

/** * Construct a type with the properties of T except for those in type K. */
// Corresponding to Pick, extract attributes from T that are not in the K set (filter operation)
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

/** * Construct a type with a set of properties K of type T */
// Construct a type with the collection elements in K as attributes and T as values
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
Copy the code

For union types

The main types of associations are: Exclude, Extract, and NonNullable

// For union types
/** * Exclude from T those types that are assignable to U */
// Remove type U from type T
type Exclude<T, U> = T extends U ? never : T;

/** * Extract from T those types that are assignable to U */
 // Select U from T
type Extract<T, U> = T extends U ? T : never;

/** * Exclude null and undefined from T */
// Remove null and undefined from T
type NonNullable<T> = T extends null | undefined ? never : T;
Copy the code

In view of the function

The main ones for functions are: Parameters, ReturnType, ConstructorParameters, InstanceType, and ThisType for this

// For functions
/** * Obtain the parameters of a function type in a tuple */
// Get the function argument type (a tuple)
type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any ? P : never;

/** * Obtain the return type of a function type */
// Get the function return value type
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;


/** * Obtain the parameters of a constructor function type in a tuple */
// Get the constructor argument type (a tuple)
type ConstructorParameters<T extends new(... args:any) = >any> = T extends new(... args: infer P) =>any ? P : never;

/** * Obtain the return type of a constructor function type */
// Get the instance type of the constructor
type InstanceType<T extends abstract new(... args:any) = >any> = T extends abstract new(... args:any) => infer R ? R : any;

/** * Marker for contextual 'this' type */
// Mark the type of this, doubt it, I have never used this type myself, the actual project is basically useless, if you want to mark this, just assign the type to this.
interface ThisType<T> { }
Copy the code

For string templates

Uppercase, Lowercase, Capitalize, and Uncapitalize are all built-in typescript implementations for string templates. Keep this tool type in mind.

// For the string template
/** * Convert string literal type to uppercase */
// Convert all characters of the string type to uppercase
type Uppercase<S extends string> = intrinsic;

/** * Convert string literal type to lowercase */
// Convert all characters of the string type to lowercase
type Lowercase<S extends string> = intrinsic;

/** * Convert first character of string literal type to uppercase */
// Convert the first character to uppercase
type Capitalize<S extends string> = intrinsic;

/** * Convert first character of string literal type to lowercase */
// Convert the first character to lowercase
type Uncapitalize<S extends string> = intrinsic;
Copy the code

Use of type protection

Type protection is also an interesting feature in typescript. It helps us narrow down type choices. Encapsulating type protection for a specific type can simplify scenarios where type assertions are used heavily.

The main type protection mechanisms come in three forms:

  • usetypeof:
    function padLeft(value: string, padding: string | number) {
        if (typeof padding === 'number') {
          return Array(padding + 1).join(' ') + value
        }
        if (typeof padding === 'string') {
          return padding + value
        }
        throw new Error(`Expected string or number, got '${padding}'. `)}Copy the code

    Note: typeofOnly two forms of type protection can be recognized:typeof v === "typename"andtypeof v ! == "typename", among them,"typename"It must be"number"."string"."boolean"or"symbol".

  • useinstanceof:
    interface Padder {
       getPaddingString(): string
    }
    
    class SpaceRepeatingPadder implements Padder {
      constructor(private numSpaces: number) {}
      getPaddingString() {
        return Array(this.numSpaces + 1).join(' ')}}class StringPadder implements Padder {
      constructor(private value: string) {}
         getPaddingString() {
            return this.value
         }
    }
    
    function getRandomPadder() {
      return Math.random() < 0.5
        ? new SpaceRepeatingPadder(4)
        : new StringPadder(' ')}/ / type for SpaceRepeatingPadder | StringPadder
    let padder: Padder = getRandomPadder()
    
    if (padder instanceof SpaceRepeatingPadder) {
      padder // Type refined to 'SpaceRepeatingPadder'
    }
    if (padder instanceof StringPadder) {
        padder // Type refinement to 'StringPadder'
    }
    Copy the code

    Note:The right side of instanceof is required to be a constructor, which typeScript refines to:

    • Of this constructorprototypeProperty if its type is notanyWords.
    • Construct the type union returned by the signature.
  • useis(Custom type protection) :
    class Fish {
      swim() {
        console.log('swim')}}class Bird {
      fly() {
        console.log('fly')}}function isFish(pet: Fish | Bird) :pet is Fish {
      return(<Fish>pet).swim ! = =undefined
    }
    Copy the code

    pet is FishIs a type predicate. The predicate forparameterName is TypeIn this form,parameterNameMust be an argument name from the current function signature, and the return value of the function must bebooleanType.

Of these, custom type protection is the most common. We can look at the following code:

const onClick = (e: MouseEvent) = > {
  const scrollLeft = (e.target as Element).scrollLeft
  const scrollTop = (e.target as Element).scrollTop
}

Copy the code

Try type protection again:

// We can do nothing, just type judgment
function isElement(target: EventTarget) :target is Element {
  return true
}
const onClick = (e: MouseEvent) = > {
  // this time e.target is EventTarget
  if (isElement(e.target)) {
    // this time e.target is Element
     const scrollLeft = e.target.scrollLeft
     const scrollTop = e.target.scrollTop
  }
}
Copy the code

As you can see, using type protection helps us optimize many responsible type assertions.

Template string type

This type comes with typescirpt after version 4.1 and comes with features that greatly simplify typescirpt writing:

type World = "world";

type Greeting = `hello ${World}`; 
// Same as type Greeting = "Hello world", similar to es6 template strings
Copy the code

In addition, the string template type automatically helps us break down union types by writing type definitions like the following:

type XDirection = 'left' | 'right'
type YDirection = 'top' | 'bottom'
type Direction = `${YDirection}-${XDirection}`
/ / equivalent type Direction = "top - left" | "top - right" | "bottom - left" | "bottom - right"
Copy the code

Of course, you can also write:

type Prefix<T extends string> = `${T}The ${string}` // The template can contain any type related to strings
const foo1: Prefix<'foo-'> = 'foo-1'
const foo2: Prefix<'foo-'> = 'foo-2'
Copy the code

The Prefix utility function above allows us to create any string type that starts with a generic parameter using the string template type, which can be used to satisfy many types in the real world.

Interestingly, string types can also be used with the infer keyword. We can separate substrings from string types that conform to a certain rule:

// We can deconstruct the matching template format
type Tool<T> = T extends `${infer P}.${infer U}`? `${P}+${U}`:never

// "a+b"
type Test1 = Tool<`a.b`>
// 'a+${number}' is available only in Typescript 4.3, not in version 4.1
type Test2 = Tool<`a.The ${number}`>
Copy the code

Typescript version 4.3 has improved the string template type to do a lot of complex and interesting things. If you’re interested, check out the documentation.

Improve paper

This article is a personal look at some usage scenarios in Typescript.

How do YOU define a type

Interface and Type

Here’s my personal opinion: use interface to describe data structures and type to describe type relationships.

  • First of all, semantically speaking, the interface interface is obvious and acts as a constraint on the objects we define. The type alias, on the other hand, is more of a naming function, that is, it’s not really a constraint function per se, it’s just a name change that simplifies the way we write types.

  • Furthermore, as TypeScript puts it, interfaces can only define the structure of object (function) types and can be inherited. The structure that type can define and the way it can be defined (for example, type Foo = typeof Foo) are more numerous and cannot inherit from other types, but can be simulated using the ampersand type.

  • Then, from the perspective of mutable and immutable, interface is mutable because of type combination, while type cannot be modified again after definition, and is immutable. The specific introduction has been given in detail in the last section, so I will not repeat it here.

As for who to use in business, the recommendation in TSLint is to give preference to interface when both interface and Type are available. In my opinion, in real life scenarios, as long as the style is consistent with the team, interface/type is not necessary except for certain scenarios (those we mentioned above).

One more tip: When you don’t know how you should define a type, use type instead.

Define subtypes appropriately

That’s the idea of modularity. Think of write definitions as write component modules.

Something like this:

interface ComponentSize = 'small' | 'default' | 'large'
interface ComponentType = 'primary' | 'warnning' | 'danger'

interface ButtonProps {
    size: ComponentSize
    type: ComponentType
}
Copy the code

Improve code cleanliness by separating out subtypes that might be used in multiple types.

Use defined types

In real development we could do this:

import React from 'react'

// Define the types of props everywhere
export interface ButtonProps {
  onClick: (e: React.MouseEvent) = > void
}
const Button: React.FC<ButtonProps> = ({ children, onClick }) = > {
  return <button onClick={onClick}>{children}</button>
}

export default Button
Copy the code
import React from 'react'
import Button, { ButtonProps } from './button'

const Demo: React.FC = () = > {
  // This is a quick way to use defined types
  const onClick: ButtonProps['onClick'] = () = > {}

  return <Button onClick={onClick} />
}
Copy the code

As you can see, we can get the onClick type directly from the existing ButtonProps type, and in some modules with strong business dependencies it is even better than using subtypes.

A case where the data is written before the type is written

A lot of people think that if you use typescript, you must write the types and then constrain the variables. Yes, this is true in most cases, but for some special cases, writing the type first is a bit redundant and makes our code redundant.

In this case, we can define the type as follows:

const obj = {
    a: 1.b: 2
}
// type
type Obj = typeof obj
Copy the code

So when do we generate a type like this? Here’s an example:

In actual development I came across a requirement to encapsulate all request functions in an API folder and pass throughrequire.context(webpack) orimport.meta.globMethods such as (vite) scan folders and encapsulate all specified request methods into an object exported for easy invocation.As some of you might have guessed, in this case, maybe we could manually define the interface that contains all the requesting functions, but as we increase the number of requesting functions, the benefits of doing so are low, and we need toDynamically generate a type that combines all the Api’s type declarations together.

For me, I usually write a plugin in development to help us automatically generate type interfaces:

// Generate the definition file based on the absolute path of the file
export function generateDeclaration(files: string[]) :string {
  const filesImport: string[] = []
  return `${files .filter((file) => ! file.endsWith('.d.ts'))
    .map((file, index) => {
      const strArr = file.split('. ')
      const len = strArr.length
      const ImportName = `File_${index}`
      filesImport.push(ImportName)
      return `import * as ${ImportName} from '${strArr
        .slice(0, len > 1 ? len - 1 : len)
        .join('. ')}'`
    })
    .join('\n')}
export type Api = ${filesImport
    .map((fileImport) => `typeof ${fileImport}`)
    .join('&')}
    `
}
Copy the code

The fs module listens to the directory during development and automatically generates the type and writes it to the specified type file every time the file is modified:

// The following is an export
import { Api } from 'xxx'
const files = require.context('. '.true./\.ts$/)
const $api: Api = {} as Api
files.keys().forEach((key) = > {
  const fileModule = files(key)
  const moduleKeys = Object.keys(fileModule) as (keyof Api)[]
  moduleKeys.forEach((moduleKey) = > {
    $api[moduleKey] = fileModule[moduleKey]
  })
})
export default $api
Copy the code

A little engineering meaning ~

However, in daily development, it is recommended to write the type first if you can, and to use typeof Value only when it would be less profitable to write the type than to write the data first (which is rare).

When to use type inference

An intuitive approach. If you’re using a typescript-friendly editor like VSCode, the auto-recognition feature of the editor is a great option. If you declare a variable that has an explicit type in the editor, you don’t need to declare the type manually, unless the type is any or does not match the type you want.

The role of generics in type inference

As mentioned earlier, generics are the soul of typescript’s inferred types. I don’t know how you usually define and use generics, but in many cases we don’t need to pass in generics manually, relying on their automatic inference of the type of variable we can get most of the type definition ideas. Let’s look at some examples from easy to difficult:

function foo<T> (bar: T) :T {
    return bar
}
Copy the code

This is the simplest use of type inference for generics. We don’t have to pass in any generic arguments when we call a function, typescript automatically infer the type for us.

, of course, to use the generic automatically infer that need to satisfy some conditions, we’ve said before, functions, classes and interfaces you can use generics, in addition to the interface was introduced into parameters must be on the inside (unless wrote a default value), the class just in the constructor parameters, the function can be specified in the parameters of the.

As above, we don’t have to pass in the type of T, because we write T in the argument that it will be derived from the value of bar.

In addition to specifying the type at the parameter, we can omit the type designation by other generics inference:

function foo<
  T extends Record<string.any>,
  R extends T & {
    test: string
  }
>(bar: T): R {
  return {
    ...bar,
    b: 2,}as R
}

// Return objects with a and b
foo({ a: 1 })
Copy the code

This way, we can also automatically infer the type of the generic (of course, we must use type assertions when using the generic inside the function).

Note, however, that typescript can’t infer the following:

// No second inference can be made in extends
function foo<T extends (. args: P) = >R.P extends any[].R> (
  bar: T,
  // The P inference actually depends on this parameter. args: P) {
  return {
    params: args,
    return: bar(... args), } }// { params:[number], return: unknown }
foo((a: number) = > a, 1)
Copy the code

That’s not true, but can’t we get the parameters and return values of the function we pass in? There are obviously other methods, and we can introduce infer to help us deduce:

function foo<T extends (. args:any[]) = >any> (bar: T, ... args: Textends(... args: infer P) =>any ? P : never
) :{
  params: T extends(... args: infer P) =>any ? P : never
  return: T extends(... args:any) => infer R ? R : any
} {
  return {
    params: args,
    return: bar(... args) } }// { params:[a:number], return: number }
foo((a: number) = > a, 1)
Copy the code

Tedious, right? That’s why we need tool types to simplify things. Using typescript’s built-in utility types looks much cleaner:

function foo<T extends (. args:any[]) = >any> (bar: T, ... args: Parameters
       ) :{
  params: Parameters<T>
  return: ReturnType<T>
} {
  return {
    params: args,
    return: bar(... args), } }// { params:[a:number], return: number }
foo((a: number) = > a, 1)
Copy the code

Type narrowing of the union type

Union types are more widely used than cross (&) types. As we all know, interaction types is a few different types of merged together into a new type, the type contains all types of properties, before and the joint type (|) said a value can be one of several types.

However, what you may not know is that union types can also give developers very precise type selection (or optional type narrowing) :

To use this capability, several different union types need to have at least one same key, and the type of the key name needs to be specified
interface TypeA {
  type: 'a'
  a: number
}
interface TypeB {
  type: 'b'
  b: number
}
interface TypeC {
  type: 'c'
  c: number
}

type UType = TypeA | TypeB | TypeC

function foo(obj: UType) {}

foo({
  type: 'a'
})
Copy the code

As we can see,When we givetypeTypeScript automatically reduces the scope of a type when it is a distinguishable value.Based on such features, we can build a system based on atypeDifferent type parameters with different complex configurations.

Definitions of global and external module types

In your daily development, you will inevitably encounter situations that add global types or add type definitions for a library. Here’s how to add a definition to them.

Global definitions

I know of two main global definition schemes:

  • One can only be defined in one withoutimportorexportkeywordtsIn the file (because if there areimportorexportWill be treated as a module) :
// global.d.ts
interface Test {
  example: string
}
// Of course, the declaration of global variables is also possible
declare const test: Test
Copy the code

Or use interface’s type merge function to extend existing global variables.

interface Window {
  example: string
}
Copy the code

This adds an Example attribute to the global Window type.

  • There is also a method called global scoping, which must be nested in an external module or in an environment module declaration. This method is defined as follows:
// Must be used in modules, i.e. with import and export keywords
// ...
// The code that defines this sentence is already in the global context
declare global {
  // Note that there is no import/export in this environment, the default is global environment
  interface Test {
    example: string
  }
  const test: Test
}
// ...
Copy the code

This is actually a bit of a stretch, I think, because many times we define global types that reference other modules’ type definitions, and we usually define global variables this way when we distribute NPM packages.

External module definition

External module definitions are often used to write or modify definition files for third-party packages, or to enable the typescript compiler to recognize file suffixes. The general writing is:

declare module 'xxx' {
    // Write detailed module definitions
    export interface XXX {}
    export const x
}
/ / or
// Short for external module definition. Default export value is any
declare module 'xxx'
Copy the code

We can use the module declaration wildcard (*) to match certain types of modules:

For example, we want typescript to recognize CSS files
declare module '*.css'
Copy the code

With this declaration, all CSS files that end in.css are recognized by typescript.

Modify the type definition of third-party packages

Changing the definition for third-party packages is a simple matter of taking advantage of external module definitions and interface’s declarative merging capabilities. The following uses the react-redux definition file as an example:

Create a react-redux.d.ts file in your own project.

// react-redux.d.ts
// It is better to add this sentence, otherwise the export may be overwritten, only DefaultRootState exists
export * from 'react-redux'

declare module "react-redux" {
    // DefaultRootState was defined as {}, we changed it to an index type
    export interface DefaultRootState {
         test:Test
        [key: string] :any}}Copy the code

If we do not do type extension, we must manually specify the type of state when we use useSelector to get the global state of Redux, otherwise we will get a type error. This extends the type of state globally without having to add arguments to each call to get the type definition.

practice

This is a hands-on section on type-oriented programming, where you’ll learn more about Typescript.

Gets the type of the KTH argument to the function

Now we will create a utility function that returns the type of any argument position of a function, like the following:

function foo(a: number, b: string) {}

// number
type A = ParamsTool<typeof foo, 0>
// string
type B = ParamsTool<typeof foo, 1>
Copy the code

In fact, the writing method is similar to the source code of Parameters writing, but in a form, with Parameters writing is like this:

function foo(a: number, b: string) {}
// number
type A = Parameters<typeof foo>[0]
// string
type B = Parameters<typeof foo>[1]
Copy the code
Please refer to 👇🏻
// Parameters
       
        [K] directly is also possible, but you need to limit the type of F
       
type ParamsTool<F, K extends number> = F extends(... args: infer P) =>any
  ? P[K] 
  : never

function foo(a: number, b: string) {}

// number
type A = ParamsTool<typeof foo, 0>
// string
type B = ParamsTool<typeof foo, 1>
Copy the code

Let me write a ref for Vue3

Ref is provided for us in Vue3, which can provide us with a responsive carrier for a certain value. Here is a brief look at its type and how to use it:

// The object generated by the ref function
export interface Ref<T = any> {
  value: T
  /** * Type differentiator only. * We need this to be in public d.ts but don't want it to show up in IDE * autocomplete, so we use a private Symbol instead. */
  [RefSymbol]: true
  / * * *@internal* /_shallow? :boolean
}
Copy the code
/ / value
const test1 = ref(0) // Ref<number>
// a Ref wrapper
const test2 = ref(test1) // Ref<number>
// double Ref wrap
const test3 = ref(test2) // Ref<number>
// Pass in the object
const test4 = ref({
  test: 0,
  test1,
  test2,
  test3
})
/* Ref<{ test: number; test1: number; test2: number; test3: number; } > * /
// Pass in an array (tuple)
const test5 = ref([
  0,
  test1,
  test2,
  test3,
  test4,
  {
    test: 0,
    test1,
    test2,
    test3
  }
] as const)
/* Ref
      
        */
      
Copy the code

As you can see, no matter we are incoming values or ref object, finally a solution package processing, get the real value of the type, there will be no nested ref type, and in the incoming objects and arrays, will make some special operations, such as the incoming object when all ref object directly to unpack on the object, When you pass in an array, you iterate over the elements of the array and make a separate judgment. Now we need to write the definition of the ref function so that it meets our expectations.

Infer can be used to determine whether the type is Ref type by defining a type that can be recursively. For the operations related to arrays and objects, the type can be determined separately by using conditional types.

Please refer to 👇🏻

This answer is not the real ref type definition, but also refer to the ref source code definition, students who want to see the source code can directly access the ref source view.

The // ref object definition simply considers value
export interface Ref<T = any> {
  value: T
}

export function ref<T> (value: T) :Ref<UnwrapRef<T>>
Copy the code

As you can see, we finally have a tool type for UnwrapRef that will help us unpack the ref.

export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRef<V>
  : T
Copy the code

Are we done? Of course not. Now we’ve just done the basic unpacking, but remember we still need to determine if the parameters passed in are objects and arrays and operate on them separately, so we need to extract one more type separately:

// Separate extract
type UnwrapRefSimple<T> = T extends Array<any>
  // If it is an array, recurse all elements
  ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
  : // Check whether it is a function or Ref (this is entered when the Ref object is in the array)
  T extends Function | Ref
  // If it is a function or Ref, return it as is
  ? T
  // If it is an object, unpack all attributes in the object
  : T extends object
  ? { [P in keyof T]: UnwrapRef<T[P]> }
  : T
Copy the code

OK, plus the definition of ref function, complete implementation is as follows:

The // ref object definition simply considers value
export interface Ref<T = any> {
  value: T
}

// Separate extract
type UnwrapRefSimple<T> = T extends Array<any>
  // If it is an array, recurse all elements
  ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
  : // Check whether it is a function or Ref (this is entered when the Ref object is in the array)
  T extends Function | Ref
  // If it is a function or Ref, return it as is
  ? T
  // If it is an object, unpack all attributes in the object
  : T extends object
  ? { [P in keyof T]: UnwrapRef<T[P]> }
  : T
export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRefSimple<V>
  : UnwrapRefSimple<T>
  
export declare function ref<T> (value: T) :Ref<UnwrapRef<T>>
Copy the code

Write a createLocalStorage bai

This function needs to implement such an effect that the type definition automatically helps us return objects with different attributes when different arguments are passed:This function is implemented as follows:

export interfacecreateLocalStorageOptions<V> { map? :(v: string | null) = >V defaultValue? : V }/ / encapsulation localStorage
// We need to automatically infer the returned object properties from the name passed in
export function createLocalStorage<T extends string.V = string | null> (name: T, options: createLocalStorageOptions
       
         = {}
       ) {
  const { defaultValue, map = (v: string | null) = > v } = options

  const action = name[0].toUpperCase() + name.slice(1)
  const setAction = `set${action}`
  const getAction = `get${action}`
  const removeAction = `remove${action}`
  const key = `${name}-key`

  return {
    [setAction](value: string) {
      localStorage.setItem(key, value)
    },
    [getAction]() {
      return map(localStorage.getItem(key)) ?? defaultValue ?? null
    },
    [removeAction]() {
      localStorage.removeItem(key)
    },
  }
}
Copy the code

Now that the return value of our function is an index type, we can think about how to improve it internally.

We’ve already mentioned typescript’s native tool types for changing the case of strings, plus conditional types that type different functions separately.

Please refer to 👇🏻
export interfacecreateLocalStorageOptions<V> { map? :(v: string | null) = >V defaultValue? : V }export function createLocalStorage<T extends string.V = string | null> (name: T, options: createLocalStorageOptions
        
          = {}
        ) {
  const { defaultValue, map = (v: string | null) = > v } = options
  // Modify the type string
  type SetAction = `set${Capitalize<T>}`
  type GetAction = `get${Capitalize<T>}`
  type RemoveAction = `remove${Capitalize<T>}`

  const action = name[0].toUpperCase() + name.slice(1)
  const setAction = `set${action}`
  const getAction = `get${action}`
  const removeAction = `remove${action}`
  const key = `local-storage-${name}-key`

  return {
    [setAction](value: string) {
      localStorage.setItem(key, value)
    },
    [getAction]() {
      return map(localStorage.getItem(key)) ?? defaultValue ?? null
    },
    [removeAction]() {
      localStorage.removeItem(key)
    },
  } as {
    // Take each child of the union type, then use the condition type to judge separately
    [P in SetAction | GetAction | RemoveAction]: P extends SetAction
      ? (value: string) = > void
      : P extends GetAction
      ? () = > V
      : () = > void}}Copy the code

Write a setState with useState

Use Estate in React is familiar, but its behavior differs from that of the React ClassComponent’s setState behavior, so we’ll encapsulate it so that it behaves like setState.

import { useState, useCallback } from 'react'

function useSetState<S extends Record<string.any> > (initial: S | (() => S)) {
  const [state, setState] = useState(initial)
  const setMergeState: SetMergeStateTool<typeof setState> = useCallback(
    (currentState) = > {
      if (typeof currentState === 'function') {
        setState((prevState) = >({... prevState, ... currentState(prevState) })) }else {
        setState((prevState) = >({... prevState, ... currentState })) } }, [] )return [state, setMergeState] as const
}

export default useSetState
Copy the code

Ok, the encapsulation method is simple, but the type writing is interesting, what we’re going to do now is just use a type tool to write our current setMergeState type based on the original setState type, how do you do that?

For a function, the most important definition is the parameter and the return value. If we can take these two things, and then modify the mapping of them, we can get the desired answer.

Please refer to 👇🏻
// If all attributes of the passed object become optional
type ObjectKeysPartial<T> = T extends(... args:any[]) = >any ? T : Partial<T>

// If you pass a function, make all the attributes of the object returned by the function optional
type FunctionReturnPartial<T> = T extends(... args: infer P) => infer R ?(. args: P) = > ObjectKeysPartial<R>
  : T
  
// setState can pass objects and functions, so we need to check whether the setState function is modified, while the setState object is modified
type setMergeStateParamsTool<T> = {
  [K in keyof T]: ObjectKeysPartial<FunctionReturnPartial<T[K]>>
}
// Modify parameters and return values
type SetMergeStateTool<T> = T extends(... args: infer P) => infer R ?(. args: setMergeStateParamsTool

) = >
R : T Copy the code

Type inference for dispatch in VUEX

What we want to achieve is that when we call Dispatch, it will automatically give us a hint of the type of the action that can match, including parsing out the type of modules in VUex

In the vuex source code we can roughly extract its type:

The following code can be briefly reviewed and will be refactored later

interfaceDispatchOptions { root? :boolean
}
interface Payload {
  type: string
}
interface Dispatch {
   // Type is a string
  (type: string, payload? :any, options? : DispatchOptions):Promise<any> <P extends Payload>( payloadWithType: P, options? : DispatchOptions ): Promise<any> } interface ActionTree<S, R> { [key: string]: Action<S, R>} // ActionHandler and ActionObject will not be discussed much, Generally is to limit the action of value type action < S, R > = ActionHandler < S, R > | ActionObject < S, R > interface ModuleTree < R > {[key: string] : Module<any, R>} interface Module<S, R> {// We can use namespaced interface Module<S, R> {// We can use namespaced interface Module<S, R> { : boolean state? : S | (() => S) // ... // modules can be nested modules? : ModuleTree<R>} interface StoreOptions<S> { : S | (() => S) // ... actions? : ActionTree<S, S> modules? : ModuleTree<S> // .. Constructor (options: StoreOptions<S>) {} Dispatch: dispatch //... } // Use const store = new store ({actions: {test() {console.log('test')}}}) store.dispatch('test')Copy the code

However, as we can see, the Dispatch itself has no indication of type, and we now need to optimize the Store’s type definition so that it can be inferred automatically.

Using a string template type, and since vuex modules can be nested, we need to create a type specific recursive type to parse modules’ types, as well as determine if namespaced is true.

Please refer to 👇🏻

We focus on the action type derivation, so the following code is only related to action and modules, if you want to write full vuex definition of friends can expand.

This type of writing is more complex, the following slowly explained:

  • First we need to redefine the generic parameters of the related type.
     // Simplify internal operations by treating actions as ordinary functions
     type Action = () = > void
     // Actions class definition
     interface ActionTree {
       [key: string]: Action
     }
     // Type definition of modules
     interface ModuleTree {
       [key: string]: Module
     }
     interfaceDispatchOptions { root? :boolean
     }
     // The function overrides the type qualification for the second mode
     interface Payload<A> {
       type: A
     }
      
     // Do not write extends to define generic parameters. This limits the inferred type
     // Attributes of the module
     interface Module<N = boolean, A = ActionTree, M = ModuleTree> {
       // We can simplify the operation, as long as the open can be nested (real case can also have many special cases)namespaced? : N actions? : AModules can be nestedmodules? : M }interface Dispatch<T> {
       // Specify the type of type here
       (type: T, payload? :any, options? : DispatchOptions):Promise<any>
       / / overloaded<P extends Payload<T>>( payloadWithType: P, options? : DispatchOptions ): Promise<any>} Constructor argument interface StoreOptions<A extends ActionTree, M extends ModuleTree> {actions? : A modules? : M} export Declare Class Store<A extends ActionTree, M extends ModuleTree> {constructor(options) StoreOptions<A, M>) dispatch: Dispatch<keyof A | GetModulesType<M>> // ... }Copy the code

    OK, now that we have modified some of the existing types, we need to add our recursive types to parsemodulesTo support thetypeSee the topDispatchOkay? The generic argument passed to it is all we can deducetypeFirst we usekeyofGet the outermost layeractionsthetypeType, and then we define a new typeGetModulesTypeJudging by the name, we rely on him to parse itmodulesThe type of.

  • Now let’s define ourGetModulesType:
    / / traverse ModuleTree
    type GetModulesType<MS extends ModuleTree> = {
      // Here we take an index inside (as a prefix) and pass it into the tool that gets the type separately
      [K in keyof MS]: GetModuleType<K, MS[K]>
    }[keyof MS] // iterate over all values
    
    // Recursively get all types associated with the Module
    type GetModuleType<P, M> = M extends Module<infer N, infer A, infer SubModules> // N is the namespaced value, A is the module's actions value (ActionTree), and SubModules is the module's value
      ? /* N extends true. Since N is Boolean, N is a combination of true and false, so both are raised. Our N is restricted to either true or false */
        N extends true
        ? // If namespaced is true, we return the actions with a prefix and all the child modules' actions, otherwise we don't prefix them
          GetPrefixActionKeys<P, A> | GetPrefixSubModules<P, SubModules>
        : GetActionKeys<A> | GetSubModules<SubModules>
      : never
    Copy the code

    After the above conversion, we can index the object’smodules(ModuleTree) to a singleModuleAnd go through all of them againModuleHere we generate four new typesGetPrefixActionKeys,GetPrefixSubModules,GetActionKeysandGetSubModules, which, as the name suggests, handles the current module separatelyactionsandmodules.

  • Finally, let’s complete the above four types:
    // Utility functions
    // Because we are passing keyof, but we only want string, so we can just take string
    type Prefix<P, K> = `${P & string}/${K & string}`
    
    // Get prefixed submodules
    Since our options are optional, we need to add two more judgments, pass the object, and the object has an index
    type GetPrefixSubModules<P, SM> = SM extends ModuleTree
      ? keyof SM extends string
        ? // This is where the recursion begins, passing submodules as modules
          Prefix<P, GetModulesType<SM>>
        : never
      : never
    // Get submodules without prefixes
    type GetSubModules<SM> = SM extends ModuleTree
      ? keyof SM extends string
        ? // Start recursion
          GetModulesType<SM>
        : never
      : never
    
    // Get the prefixed actions keys
    Since our options are optional, we need to add two more judgments, pass the object, and the object has an index
    type GetPrefixActionKeys<P, A> = A extends ActionTree
      ? keyof A extends string
        ? Prefix<P, keyof A>
        : never
      : never
    // Get the keys of actions without a prefix
    type GetActionKeys<A> = A extends ActionTree
      ? keyof A extends string
        ? keyof A
        : never
      : never
    Copy the code

    We’re done. InGetPrefixSubModulesandGetSubModulesUse recursion to convert to the moduletypeGet, and finally get the type.

Take a test:

// test
const store = new Store({
  actions: {
    root(){}},modules: {
    cart: {
      namespaced: true.actions: {
        add() {},
        remove(){}}},user: {
      namespaced: true.actions: {
        login(){}},modules: {
        admin: {
          namespaced: true.actions: {
            adminLogin(){}},modules: {
            goods: {
              / / namespaced to false
              namespaced: false.actions: {
                add(){}}}}},editor: {
          namespaced: true.actions: {
            editorLogin(){}},modules: {
            post: {
              / / don't write namespaced
              actions: {
                create() {}
              }
            }
          }
        }
      }
    }
  }
})

store.dispatch('root')
Copy the code

Perfect!

Complete code:

// Utility functions
// Because we are passing keyof, but we only want string, so we can just take string
type Prefix<P, K> = `${P & string}/${K & string}`

// Get prefixed submodules
Since our options are optional, we need to add two more judgments, pass the object, and the object has an index
type GetPrefixSubModules<P, SM> = SM extends ModuleTree
  ? keyof SM extends string
    ? // This is where the recursion begins, passing submodules as modules
      Prefix<P, GetModulesType<SM>>
    : never
  : never
// Get submodules without prefixes
type GetSubModules<SM> = SM extends ModuleTree
  ? keyof SM extends string
    ? // Start recursion
      GetModulesType<SM>
    : never
  : never

// Get the prefixed actions keys
Since our options are optional, we need to add two more judgments, pass the object, and the object has an index
type GetPrefixActionKeys<P, A> = A extends ActionTree
  ? keyof A extends string
    ? Prefix<P, keyof A>
    : never
  : never
// Get the keys of actions without a prefix
type GetActionKeys<A> = A extends ActionTree
  ? keyof A extends string
    ? keyof A
    : never
  : never

// Simplify internal operations by treating actions as ordinary functions
type Action = () = > void
interface ActionTree {
  [key: string]: Action
}
interface ModuleTree {
  [key: string]: Module
}

// Do not write extends here. It limits the type of inference involved
interface Module<N = boolean, A = ActionTree, M = ModuleTree> {
  // We can simplify the operation, as long as the open can be nested (real case can also have many special cases)namespaced? : N actions? : AModules can be nestedmodules? : M }/ / traverse ModuleTree
type GetModulesType<MS extends ModuleTree> = {
  // Here we take an index inside (as a prefix) and pass it into the tool that gets the type separately
  [K in keyof MS]: GetModuleType<K, MS[K]>
}[keyof MS] // iterate over all values

// Recursively get all types associated with the Module
type GetModuleType<P, M> = M extends Module<infer N, infer A, infer SubModules> // N is the namespaced value, A is the module's actions value (ActionTree), and SubModules is the module's value
  ? /* N extends true. Since N is Boolean, N is a combination of true and false, so both are raised. Our N is restricted to either true or false */
    N extends true
    ? // If namespaced is true, we return the actions with a prefix and all the child modules' actions, otherwise we don't prefix them
      GetPrefixActionKeys<P, A> | GetPrefixSubModules<P, SubModules>
    : GetActionKeys<A> | GetSubModules<SubModules>
  : never

interface StoreOptions<A extends ActionTree, M extendsModuleTree> { actions? : A modules? : M }interfaceDispatchOptions { root? :boolean
}
interface Payload<A> {
  type: A
}
interface Dispatch<T> {
  (type: T, payload? :any, options? : DispatchOptions):Promise<any> <P extends Payload<T>>( payloadWithType: P, options? : DispatchOptions ): Promise<any>} export Declare Class Store<A extends ActionTree, M extends ModuleTree> { Do not speak code implements the constructor (options: StoreOptions < > A, M) dispatch: dispatch < keyof A | GetModulesType < M > > / /... } // test const store = new Store({ actions: { root() {} }, modules: { cart: { namespaced: true, actions: { add() {}, remove() {} } }, user: { namespaced: true, actions: { login() {} }, modules: { admin: { namespaced: // Namespaced: false, actions: {adminLogin() {}}, modules: {goods: {// namespaced: false, actions: { add() {} } } } }, editor: { namespaced: true, actions: { editorLogin() {} }, modules: { post: {// don't write namespaced actions: {create() {}}}}}}}}}) store.dispatch('root')Copy the code

conclusion

The length of this article is very long, many places can be isolated as an article, but I feel it is not conducive to the later collation and reference, so I wrote together, of course, if you feel that the viewing effect is not very good, you can also put forward, later will make improvements.

Again, this article is just a summary of some of the personal practices I’ve learned in typescript. It’s not necessarily correct, and it doesn’t introduce too many complex types, but typescript is certainly capable of much more than that. There are many examples of heavily typescript typed programming. However, I hope this article has helped you get a feel for how typescript works in your projects. If you can work your way through a few of the typescript type definitions in this article, you’ll be able to improve your typescript capabilities.

Finally, this paper will inevitably have some summary is not in place, please point out or supplement in time, the author will be corrected in the first time. If you find this article helpful, please visit 👍🏻.

reference

  • TypeScript official documentation
  • Ref definition of Vue
  • Vuex definition file
  • A new feature in TS 4.1 enables Dispatch type inference for Vuex infinite hierarchy namespaces