preface

In 2020, TS is becoming more and more popular, whether it is the server (Node.js) or the front-end framework (Angular, Vue3), there are more and more projects using TS development, as a front-end programmer, TS has become an essential skill. The purpose of this article is to introduce some of the advanced techniques in TS to improve your understanding of the language at a deeper level.

Introduction of Typescript

  • ECMAScript superset (Stage 3)
  • Type checking at compile time
  • No overhead introduced (zero dependencies, no extension of JS syntax, no intrusion into the runtime)
  • Compile generic, readable JS code

Typescript = Type + ECMAScript + Babel-Lite

Typescript design goals: github.com/Microsoft/T…

Why use Typescript

  • Increased code readability and maintainability
  • Fewer runtime errors, safer code and fewer bugs
  • Enjoy the benefits of code hints
  • Refactoring artifact

The base type

  • boolean
  • number
  • string
  • array
  • tuple
  • enum
  • void
  • null & undefined
  • any & unknown
  • never

anyunknownThe difference between

  • any: Any type
  • unknown: An unknown type

Any type can be assigned to unknown, but unknown cannot be assigned to any other basic type, while any can be assigned and assigned to anything.

let foo: unknown

foo = true // ok
foo = 123 //ok

foo.toFixed(2) // error

let foo1: string = foo // error
Copy the code
let bar: any

bar = true // ok
bar = 123 //ok

foo.toFixed(2) // ok

let bar1:string  = bar // ok
Copy the code

As you can see, using any is equivalent to completely missing type checking, so use any as little as possible, and use unknown for unknown types.

Correct usage of unknown

We can narrow the unknown type down to a more specific range of types in different ways:

function getLen(value: unknown) :number {
  if (typeof value === 'string') {
    // Value is considered to be a string type because of type protection
  	return value.length
  }
  
  return 0
}
Copy the code

This process is called Type narrowing.

never

Never Indicates the types that cannot be reached by users. In the latest typescript 3.7, the following code will report an error:

// never User control flow analysis
function neverReach () :never {
  throw new Error('an error')}const x = 2

neverReach()

x.toFixed(2)  // x is unreachable
Copy the code

Never can also be used for unids of union types:

type T0 = string | number | never // T0 is string | number
Copy the code

Function types

The return value type of several function types

function fn() :number {
  return 1
}

const fn = function () :number {
  return 1
}

const fn = (): number= > {
  return 1
}

const obj = {
  fn (): number {
    return 1}}Copy the code

Just add the return value type after ().

Function types

Ts also has function types that describe a function:

type FnType = (x: number, y: number) = > number
Copy the code

The complete way to write a function

let myAdd: (x: number, y: number) = > number = function(x: number, y: number) :number {
  return x + y
}

// Use the FnType
let myAdd: FnType = function(x: number, y: number) :number {
  return x + y
}

// ts automatically deduces parameter types
let myAdd: FnType = function(x, y) {
  return x + y
}
Copy the code

Function overload?

Because js is a dynamic type, it does not need to support overloading. To ensure type safety, TS supports type overloading of function signatures. That is:

multipleReloading the signatureAnd aTo realize the signature

// Overloaded signature (function type definition)
function toString(x: string) :string;
function toString(x: number) :string;

// Implement signature (function body)
function toString(x: string | number) {
  return String(x)
}

let a = toString('hello') // ok
let b = toString(2) // ok
let c = toString(true) // error
Copy the code

If you defineReloading the signature,To realize the signatureInvisible to the public

function toString(x: string) :string;

function toString(x: number) :string {
  return String(x)
}

len(2) // error
Copy the code

To realize the signatureMust be compatible withReloading the signature

function toString(x: string) :string;
function toString(x: number) :string; // error

// Function implementation
function toString(x: string) {
  return String(x)
}
Copy the code

Reloading the signatureThe types of are not merged

// Overloaded signature (function type definition)
function toString(x: string) :string;
function toString(x: number) :string;

// Implement signature (function body)
function toString(x: string | number) {
  return String(x)
}

function stringOrNumber(x) :string | number {
  return x ? ' ' : 0
}

// Input is the union type of string and number
/ / the string | number
const input = stringOrNumber(1)

toString('hello') // ok
toString(2) // ok
toString(input) // error
Copy the code

Type inference

Type inference in TS is very powerful, and its internal implementation is complex.

Basic type inference:

// ts deduces that x is of type number
let x = 10
Copy the code

Object type inference:

MyObj: {x: number; y: string; z: boolean; }
const myObj = {
  x: 1,
  y: '2',
  z: true
}
Copy the code

Function type inference:

// ts deduces that the return value of the function is of type number
function len (str: string) {
  return str.length
}
Copy the code

Context type inference:

// ts deduces that event is of type ProgressEvent
const xhr = new XMLHttpRequest()
xhr.onload = function (event) {}
Copy the code

So sometimes you don’t have to declare the type manually for simple types, and let TS infer it.

Type compatibility

Typescript subtypes are based on structure subtypes, as long as the structure is compatible. (Duck Type)

class Point {
  x: number
}

function getPointX(point: Point) {
  return point.x
}

class Point2 {
  x: number
}

let point2 = new Point2()

getPointX(point2) // OK
Copy the code

Traditional statically typed languages such as Java and c++ are based on nominal subtypes and must display declared subtype relationships (inheritance) to be compatible.

public class Main {
  public static void main (String[] args) {
    getPointX(new Point()); // ok
    getPointX(new ChildPoint()); // ok
    getPointX(new Point1());  // error
  }

  public static void getPointX (Point point) {
    System.out.println(point.x);
  }

  static class Point {
    public int x = 1;
  }

  static class Point2 {
    public int x = 2;
  }
    
  static class ChildPoint extends Point {
    public int x = 3; }}Copy the code

Object subtype

The subtype must contain all the properties and methods of the source type:

function getPointX(point: { x: number }) {
  return point.x
}

const point = {
	x: 1,
  y: '2'
}

getPointX(point) // OK
Copy the code

Note: An error will be reported if an object literal is passed directly:

function getPointX(point: { x: number }) {
  return point.x
}

getPointX({ x: 1, y: '2' }) // error
Copy the code

This is another feature in TS called excess Property check, which is used when an object literal is passed in as an argument.

Function subtype

Before introducing function subtypes, let me introduce the concepts of contravariant and covariant. Contravariant and covariant concepts are not unique to TS. They exist in other static languages as well.

Before the introduction, let’s assume a problem. The convention is to mark it as follows:

  • A ≼ BIndicates that A is A subtype of B and that A contains all properties and methods of B.
  • A => BRepresents A method that takes A and B as the return value.(param: A) => B

If we now have three types of Animal, Dog, and WangCai, then there must be the following relationship:

WangCai ≼ Dog ≼ Animal WangCai ≼ Dog ≼ AnimalCopy the code

Question: Which of the following types is a subclass of Dog => Dog?

  • WangCai => WangCai
  • WangCai => Animal
  • Animal  => Animal
  • Animal  => WangCai

Look at it in code

Class Animal {sleep: Function} class Dog extends Animal {// bark: Function} class WangCai extends Dog {dance: Function} Function } function getDogName (cb: (dog: Const Dog = cb(new Dog()) dog.bark()} // For the incoming argument, WangCai is a subclass of Dog, and there is no dance method on Dog. // For an argument, the WangCai class inherits from the Dog class, so there must be a bark method getDogName((WangCai: WangCai) => {wangcai.dance() return new WangCai()}) // For the Animal parameter, there is no bark method on the Animal class, generating an exception. getDogName((wangcai: WangCai) => {wangcai.dance() return new Animal()}); // The WangCai class inherits from the Dog class, so there must be a bark method getDogName(animal: Animal) => {Animal.sleep() return new WangCai()}) => {Animal.sleep() return new WangCai()}) // For the Animal parameter, there is no bark method on the Animal class, generating an exception. getDogName((animal: Animal) => { animal.sleep() return new Animal() })Copy the code

We can see that only Animal => WangCai is a subtype of Dog => Dog. We can conclude that for function types, the type compatibility of function parameters is reversed, which is called contravariant, and the type compatibility of return values is forward, which is called covariant.

The examples of contravariant and covariant only illustrate the case when a function has only one argument. How to distinguish between multiple arguments?

Function arguments can be converted to Tuple type compatibility:

type Tuple1 = [string.number]
type Tuple2 = [string.number.boolean]

let tuple1: Tuple1 = ['1'.1]
let tuple2: Tuple2 = ['1'.1.true]

let t1: Tuple1 = tuple2 // ok
let t2: Tuple2 = tuple1 // error
Copy the code

(Tuple2 => Tuple1); (Tuple2 => Tuple1); (Tuple2 => Tuple1); (Tuple2 => Tuple1); (Tuple2 => Tuple1);

[1.2].forEach((item, index) = > {
	console.log(item)
}) // ok

[1.2].forEach((item, index, arr, other) = > {
	console.log(other)
}) // error
Copy the code

High-level types

Union types and crossover types

A union type represents multiple types of “or” relationships

function genLen(x: string | any[]) {
  return x.length
}

genLen(' ') // ok
genLen([]) // ok
genLen(1) // error
Copy the code

Crossover types represent multiple types of “and” relationships

interface Person {
  name: string
  age: number
}

interface Animal {
  name: string
  color: string
}

const x: Person & Animal = {
  name: 'x',
  age: 1,
  color: 'red
}
Copy the code

Use union types to represent enumerations

type Position = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'

const position: Position = 'UP'
Copy the code

You can avoid using enums to invade the runtime.

Type of protection

Ts beginners can easily write the following code:

function isString (value) {
  return Object.prototype.toString.call(value) === '[object String]'
}

function fn (x: string | number) {
  if (isString(x)) {
    return x.length / / the error type "string | number" there is no attribute "length".
  } else {
    / /...}}Copy the code

How do you get TS to infer the type of context?

1. Use TSiskeywords

function isString (value: unknown) :value is string {
  return Object.prototype.toString.call(value) === '[object String]'
}

function fn (x: string | number) {
  if (isString(x)) {
    return x.length
  } else {
    / /...}}Copy the code

Typeof keywords

In ts, the typeof keyword in the code implementation helps ts determine the basic typeof the variable:

function fn (x: string | number) {
  if (typeof x === 'string') { // x is string
    return x.length
  } else { // x is number
    / /...}}Copy the code

3. Instanceof

In TS, the instanceof keyword helps ts determine the type of the constructor:

function fn1 (x: XMLHttpRequest | string) {
  if (x instanceof XMLHttpRequest) { // x is XMLHttpRequest
    return x.getAllResponseHeaders()
  } else { // x is string
    return x.length
  }
}
Copy the code

4. Type protection for NULL and undefined

In conditional judgments, ts automatically protects null and undefined:

function fn2 (x? :string) {
  if (x) {
    return x.length
  }
}
Copy the code

5. Type assertions for null and undefined

If we already know that the parameter is not empty, we can use! To manually mark:

function fn2 (x? :string) {
  returnx! .length }Copy the code

Typeof keywords

In addition to protecting types, typeof keywords can derive types from implementations.

Note: Typeof is a type keyword and can only be used in type syntax.

function fn(x: string) {
  return x.length
}

const obj = {
  x: 1,
  y: '2'
}

type T0 = typeof fn // (x: string) => number
type T1 = typeof obj // {x: number; y: string }
Copy the code

Keyof keywords

Keyof is also a type keyword that can be used to retrieve all key values for an object interface:

interface Person {
  name: string
  age: number
}

type PersonAttrs = keyof Person // 'name' | 'age'
Copy the code

In the key word

In is also a type keyword that can be traversed over the union type and can only be used under the type keyword.

type Person = {
  [key in 'name' | 'age'] :number
}

// { name: number; age: number; }
Copy the code

The [] operator

Index access is possible using the [] operator, which is also a type keyword

interface Person {
  name: string
  age: number
}

type x = Person['name'] // x is string
Copy the code

A small chestnut

Write a type tool for a type copy:

type Copy<T> = {
  [key in keyof T]: T[key]
}

interface Person {
  name: string
  age: number
}

type Person1 = Copy<Person>
Copy the code

The generic

A generic is a parameter of a type. In TS, a generic can be used for classes, interfaces, methods, type aliases, and other entities.

A profound

function createList<T> () :T[] {
  return [] as T[]
}

const numberList = createList<number> ()// number[]
const stringList = createList<string> ()// string[]
Copy the code

With generics support, the createList method can pass in a type and return a typed array instead of an any[].

Generic constraint

If we only want the createList function to generate arrays of specified types, we can use the extends keyword to restrict the scope and shape of generics.

type Lengthwise = {
  length: number
}

function createList<T extends number | Lengthwise> () :T[] {
  return [] as T[]
}

const numberList = createList<number> ()// ok
const stringList = createList<string> ()// ok
const arrayList = createList<any[] > ()// ok
const boolList = createList<boolean> ()// error
Copy the code

Any [] is an array type. Array types have a length attribute, so ok. String also has a length attribute, so ok. But Boolean can’t pass this constraint.

Under controlled conditions

Extends constraint type, in addition to do can do under controlled conditions, equivalent to a ternary operator, is only for the type.

Expression: T extends U? X : Y

Meaning: return X if T can be assigned to U, Y otherwise. In general, T is said to be assigned to U if T is a subtype of U. For example:

type IsNumber<T> = T extends number ? true : false

type x = IsNumber<string>  // false
Copy the code

Mapping type

Mapping type is similar to a function of one type. It can do some type operations, input one type, output another type, we used the example of Copy.

There are several built-in mapping types

// Each attribute becomes optional
type Partial<T> = {
  [P inkeyof T]? : T[P] }// Each property becomes read-only
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// Select some properties in the object
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

/ /...
Copy the code

Typescript 2.8 has several mapping types built into lib.d.ts:

  • Partial<T>TAll properties in the
  • Readonly<T>TAll properties in the
  • Pick<T, U>– chooseTCan be assigned toUThe type of.
  • Exclude<T, U>– fromTCan be assigned toUThe type of.
  • Extract<T, U>Extract –TCan be assigned toUThe type of.
  • NonNullable<T>– fromTIn rejectingnullandundefined.
  • ReturnType<T>— Gets the return type of the function.
  • InstanceType<T>Gets the instance type of the constructor type.

So when we write TS, we can use these types of tools directly:

interface ApiRes {
  code: string;
  flag: string;
  message: string;
  data: object;
  success: boolean;
  error: boolean;
}

type IApiRes = Pick<ApiRes, 'code' | 'flag' | 'message' | 'data'>

/ / {
// code: string;
// flag: string;
// message: string;
// data: object;
// }
Copy the code

Extends condition distribution

For T extends U, right? For X: Y, there is also a feature of conditional distribution when T is a union type.

type Union = string | number
type isNumber<T> = T extends number ? 'isNumber' : 'notNumber'

type UnionType = isNumber<Union> // 'notNumber' | 'isNumber'
Copy the code

In fact, extends takes the following form:

(string extends number ? 'isNumber' : 'notNumber') | (number extends number ? 'isNumber' : 'notNumber')
Copy the code

Extract is based on this feature, combined with the characteristic of never:

type Exclude<T, K> = T extends K ? never : T

type T1 = Exclude<string | number | boolean.string | boolean>  // number
Copy the code

inferkeywords

Infer can store the types in the operation process. The built-in ReturnType is realized based on this characteristic:

type ReturnType<T> = 
  T extends(... args:any) => infer R ? R : never

type Fn = (str: string) = > number

type FnReturn = ReturnType<Fn> // number
Copy the code

The module

Global modules vs. file modules

By default, we write code that is located under the global module:

const foo = 2
Copy the code

At this point, if we create another file and write the following code, TS considers this to be normal:

const bar = foo // ok
Copy the code

To break this restriction, all you need is an import or export expression in the file:

export const bar = foo // error
Copy the code

Module resolution strategy

Tpescript has two module resolution strategies: Node and Classic. When module is set to AMD, System, or ES2015 in tsconfig.json, the default is classic; otherwise, it is Node. You can also manually specify the module resolution policy using moduleResolution.

The difference between the two module resolution strategies is that for the following module introductions:

import moduleB from 'moduleB'
Copy the code

Classic mode path addressing:

/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Copy the code

Node mode path addressing:

/root/src/node_modules/moduleB.ts /root/src/node_modules/moduleB.tsx /root/src/node_modules/moduleB.d.ts / root/SRC/node_modules/moduleB/package. The json (if specified"types"Attributes)/root/SRC/node_modules/moduleB/index. The ts/root/SRC/node_modules/moduleB/index. The TSX /root/src/node_modules/moduleB/index.d.ts /root/node_modules/moduleB.ts /root/node_modules/moduleB.tsx / root/node_modules/moduleB which s/root/node_modules/moduleB/package. The json (if specified"types"Attributes)/root/node_modules/moduleB/index. The ts/root/node_modules/moduleB/index. The TSX/root/node_modules/moduleB/index. Which s / node_modules/moduleB. Ts/node_modules/moduleB TSX/node_modules/moduleB which s/node_modules/moduleB/package. The json (if specified"types"Attributes)/node_modules/moduleB/index. Ts/node_modules/moduleB/index. The TSX/node_modules/moduleB/index. Which sCopy the code

Declaration file

What is a declaration file

The.d.ts declaration file is used to describe the structure of the code and is usually used to provide type definitions for the JS library.

When you install a package using NPM and use it, you will get a syntax hint for the package. Here is the vue hint:

The jSONp library’s declaration file is a simple example of what a declaration file looks like:

type CancelFn = (a)= > void;
type RequestCallback = (error: Error | null, data: any) = > void;

interfaceOptions { param? :string; prefix? :string; name? :string; timeout? :number;
}

declare function jsonp(url: string, options? : Options, cb? : RequestCallback) :CancelFn;
declare function jsonp(url: string, callback? : RequestCallback) :CancelFn;

export = jsonp;
Copy the code

With this declaration, the editor can use this declaration to make syntax hints when using the library.

How does the editor find this declaration file?

  • If there is one in the root directory of the packageindex.d.tsThis is the declaration file for the library.
  • If this is a bagpackage.jsonThere aretypesortypingsField, which refers to the package declaration file.

If a library has not been maintained for a long time, or if the author has disappeared, typescript officially provides a repository of declarations. Try installing declarations for a library with the @types prefix:

npm i @types/lodash
Copy the code

When lodash is introduced, the editor will also try to look for node_modules/@types/lodash to give you a hint about the syntax of lodash.

Another option is to write your own declaration file. The editor collects declarations locally for your project. If a package doesn’t have a declaration file and you want a syntax hint, you can write your own declaration file locally:

// types/lodash.d.ts
declare module "lodash" {
  export function chunk(array: any[], size? :number) :any[];
  export function get(source: any, path: string, defaultValue? :any) :any;
}
Copy the code

If the source code is written in ts, when it is translated into JS, just add -d to generate the corresponding declaration file.

tsc -d
Copy the code

Declaration file how to write can refer to www.tslang.cn/docs/handbo…

Also note that if a library has a declaration file, the editor doesn’t care about the library code anymore, it just prompts based on the declaration file.

Extending native objects

If you’ve written ts, you’re wondering, how do I customize properties on the Window object?

window.myprop = 1 // error
Copy the code

By default, myprop does not exist on Windows, so it is not possible to assign a value directly. Of course, you can use square brackets to assign a value, but you must also use [] for get operations, and there is no type prompt.

window['myprop'] = 1 // OK

window.myprop  // Attribute "myprop" does not exist on type "Window & typeof globalThis"
window['myprop'] // ok, but no prompt, no type
Copy the code

At this point, you can use the declaration file to extend other objects by creating any xxx.d.ts in your project:

// index.d.ts
interface Window {
  myprop: number
}

// index.ts
window.myprop = 2  // ok
Copy the code

It is also possible to extend global objects within modules:

import A from 'moduleA'

window.myprop = 2

declare global {
  interface Window {
    myprop: number}}Copy the code

Extend other modules

If you’ve ever used TS to write vue, you’ve probably encountered this problem. How do you extend the properties or methods on vue.prototype?

import Vue from 'vue'

Vue.prototype.myprops = 1

const vm = new Vue({
  el: '#app'
})

CombinedVueInstance
      
       >
      ,>
// Attribute "myprops" does not exist
console.log(vm.myprops)
Copy the code

To extend the properties on the vue instance in the project’s XXX.d. ts:

import Vue from 'vue'

declare module 'vue/types/vue' {
  interface Vue {
    myprop: number}}Copy the code

Ts provides the declare Module ‘XXX’ syntax to extend other modules, which is useful for add-on libraries and packages, such as the Vue-router extension Vue.

// vue-router/types/vue.d.ts
import Vue from 'vue'
import VueRouter, { Route, RawLocation, NavigationGuard } from './index'

declare module 'vue/types/vue' {
  interface Vue {
    $router: VueRouter
    $route: Route
  }
}

declare module 'vue/types/options' {
  interface ComponentOptions<V extendsVue> { router? : VueRouter beforeRouteEnter? : NavigationGuard<V> beforeRouteLeave? : NavigationGuard<V> beforeRouteUpdate? : NavigationGuard<V> } }Copy the code

How to handle non-JS files, for example.vueFile import?

To deal withvuefile

For all files ending in.vue, the vue type can be exported by default, which complies with the rules for vUE single-file components.

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}
Copy the code

Handle CSS in JS

For all.css, you can export a value of type any by default, which solves the problem but misses the type checking.

declare module '*.css' {
	const content: any
  export default content
}
Copy the code
import * as React from 'react' import * as styles from './index.css' const Error = () => ( <div ">😭</div> <p className={styles. Title}>Ooooops! </p> <p>This page doesn't exist anymore.</p> </div> ) export default ErrorCopy the code

In fact, both global extension and module extension are based on the feature of TS declaration merging. Simply put, TS will merge some interface, class and type aliases with the same name collected by it according to certain rules.

compile

Ts has a built-in compiler (TSC) that allows us to compile TS files into JS files. With a variety of compilation options, sometimes we can do most of the work without Babel.

Common compilation options

When TSC compiles TS code, it takes a different compilation strategy based on the tsconfig.json configuration file options. Here are three common configuration items:

  • Target – The JS language version of the generated code, such as ES3, ES5, ES2015, etc.
  • Module – The module system that the generated code needs to support, such as ES2015, CommonJS, UMD, etc.
  • Lib – Tells TS what features are in the target environment, such as WebWorker, ES2015, DOM, etc.

Like Babel, TS converts only new syntax, not new apis, at compile time, so some scenarios need to handle polyfills themselves.

Change the compiled directory

The outDir field in tsconfig can configure the compiled file directory, which is conducive to unified management of dist.

{
  "compilerOptions": {
    "module": "umd"."outDir": "./dist"}}Copy the code

The compiled directory structure:

Myproject ├── dist │ ├── index.js │ ├── libr.js │ ├── libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.tsCopy the code

The output is compiled to a JS file

For AMD and System modules, you can configure the outFile field in tsconfig.json to output a JS file. What if I need to output to another module, such as UMD, and I want to package it into a separate file? You can use rollup or Webpack:

// rollup.config.js
const typescript = require('rollup-plugin-typescript2')

module.exports = {
  input: './index.ts'.output: {
    name: 'MyBundle'.file: './dist/bundle.js'.format: 'umd'
  },
  plugins: [
    typescript()
  ]
}
Copy the code

Some common TS peripheral libraries

A tip to improve development efficiency

For example, import XXX from ‘@/path/to/name’. If the editor does not do any configuration, this will be embarrassing. The compiler will not give you any path hints, let alone syntax hints. You can also configure the paths in the.ts file as well as the syntax hint for the paths.

You can rename tsconfig.json to jsconfig.json. You can also use the path alias hint and syntax hint in.js files.

If you want to use WebStorm, just open Settings, search for Webpack, and set the path to the Webpack configuration file.

Learning to recommend

  • Typescript Chinese website
  • Introduction to Typescript
  • github – awesome-typescript
  • Zhihu – Come play TypeScript, you’ve got it on!
  • Conditional-types-in-typescript (conditional types in TS)