一万字总结,搞定TypeScript

1,095 阅读28分钟

前言


ts已经是前端必知必会的,未来只会更火,项目中也将应用的更多!

学起来吧!

本文前边部分比较基础,后半部分较深入,希望耐心看完,或多或少,一定会有收获!

一、ts简单介绍


TypeScriptJavaScript的超集,遵循ES6/ES5规范,扩展了JavaScript的语法。

TypeScript最大特点是提示友好,对代码进行类型检查,编码阶段发现错误,避免线上问题。ts不能直接在浏览器运行,最终还是会被编译成js。

下面进入正题~

二、ts环境配置


1.全局安装TypeScript,对ts进行编译。

npm install typescript -g
tsc --init  # 用来生成tsconfig.json配置文件
tsc # 可以将ts文件编译成js文件
tsc --watch # 监控ts文件变化生成js文件

用node环境执行ts:
① vscode插件:code runner
② 全局安装 ts-node -g

2.构建工具环境配置

ts的解析方式有两种:

  • ts插件解析
  • babel解析 rollup => rollup-plugin-typescript2
    webpack => ts-loader babel-plugin-typescript
  • 安装依赖
npm install rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-serve -D

rollup-plugin-typescript2rolluptypescript沟通的桥梁。
@rollup/plugin-node-resolve 让rollup支持解析第三方模块。
rollup-plugin-serve 启动服务。

  • 初始化ts配置文件
npx tsc --init
  • rollup.config.js配置
import ts from 'rollup-plugin-typescript2'
import {nodeResolve} from '@rollup/plugin-node-resolve';
import serve from 'rollup-plugin-serve';
import path from 'path'
export default {
    input:'src/index.ts',
    output:{
        format:'iife', // umd, esm, cjs iife 
        file:path.resolve('dist/bundle.js'), 
        sourcemap:true
    },
    plugins:[
        nodeResolve({
            extensions:['.js','.ts']
        }),
        ts({
            tsconfig:path.resolve(__dirname,'tsconfig.json')
        }),
        serve({
            open:true,
            openPage:'/public/index.html',
            port:3000,
            contentBase:'' // 表示以根目录为基准
        })
    ]
}
  • package.json配置
"scripts": {
    "dev": "rollup -c -w"
}

npm run dev 搞起!

三、基础类型


1.布尔、数字、字符串类型

let bool:boolean = true;
let num:number = 10;
let str:string = 'hello world';

2.元组类型

类型对应,个数固定

let tuple: [string, number, boolean] = ['hello', 10, true]; // 初始化时,必须按照类型定义进行赋值
tuple.push('world') // 只能增加定义过的类型
tuple[3] = 100; // 不能通过索引改变元组

3.数组

声明数组中元素数据类型

let arr1: number[] = [1, 2, 3];
let arr2: string[] = ['1', '2', '3'];
let arr3: (number | string)[] = [1, '2', 3];
let arr4: Array<number | string> = [1, '2', 3];

4.枚举类型

  • 普通枚举
enum LANG {
    REACT,
    VUE,
    ANGULAR
}
// {0: "REACT", 1: "VUE", 2: "ANGULAR", REACT: 0, VUE: 1, ANGULAR: 2}
// 编译后的结果
(function (LANG) {
    LANG[USER_ROLE["REACT"] = 0] = "REACT";
    LANG[USER_ROLE["VUE"] = 1] = "VUE";
    LANG[USER_ROLE["ANGULAR"] = 2] = "ANGULAR";
})(LANG || (LANG = {}));
  • 异构枚举
enum LANG {
    REACT = 'react',
    VUE = 1,
    ANGULAR
}
// {"REACT": "react", "VUE": 1, "ANGULAR": 2}

枚举支持反举,但仅限于索引,会自动进行推断。

  • 常量枚举
enum LANG {
    REACT,
    VUE,
    ANGULAR
}
console.log(LANG.VUE, LANG[0]) // 1 REACT

const enum LANG {
    REACT,
    VUE,
    ANGULAR
}
console.log(LANG.REACT, LANG[0]) // 0 undefined

加上const后,不会生成一个对象,就不支持反举了。

5.any类型

6.null和undefined

nullundefined是任意类型的子类型。
如果strictNullChecks值为true,或者严格模式下,则不能把nullundefined赋给其他类型,null只能赋值给nullundefined只能赋值给undefined

let xx = number | boolean;
xx = null;

7.void类型

void的值只能赋予null和undefined,一般用于函数的返回值。

let a: void; // a = undefined;
let b: () => void; // 表示没有返回值,返回值是undefined
interface IC {
    new (): void; // 表示不关心返回值类型
}

严格模式下,不能将null赋值给void。 void用在接口中定义类中原型方法时,表示不关心返回值类型。在函数返回值中表示返回值是undefined,即没有返回值。

8.never类型

任何类型的子类型,表示永远不会出现的值。
不能把其他类型的值赋给never。

// 抛错
function error(message: string): never {
    throw new Error("err");
}
// 永不结束的循环
function loop(): never {
    while (true) { }
}
// 永远走不进去的条件
function fn(x:number | string){
    if(typeof x == 'number'){

    }else if(typeof x === 'string'){

    }else{
        console.log(x); // never
    }
}

9.object对象类型

object表示非原始类型。除number,string,boolean,symbolnullundefined之外的类型。

let create = (obj:object):void=>{}
create({});
create([]);
create(function(){})

泛型约束会大量用object类型。

10.unknown 类型

1) unknown类型

表示类型未知,任何类型都可以赋值为unknown类型。它是any类型对应的安全类型。

let unknown:unknown;
unknown = 'tianshi';
unknown = 18;

不能访问unknown类型上的属性,不能作为函数、类来使用。

  • 联合类型中的unknown
type UnionUnknown = unknown | null | string | number;  // type UnionUnknown = unknown

其他类型与unknown联合,结果都是unknown类型。

  • 交叉类型中的unknown
type inter = unknown & null; // type inter = null

其他类型与unknown交叉,结果都是其他类型。

2) unknown特性
  • neverunknown的子类型
type isNever = never extends unknown ? true : false; // type isNever = true
  • keyof unknownnever
type key = keyof unknown; // type key = never
  • unknown类型不能被遍历
type IMap<T> = {
    [P in keyof T]: number
}
type t = IMap<unknown>; // type t = {}
  • unknown类型不能和number类型进行+运算
let p1: unknown;
let p2: number;
let p = p1 + p2 // 类型报错

11.Symbol类型

Symbol表示独一无二

const s1 = Symbol('key');
const s2 = Symbol('key');
console.log(s1 == s2); // false

12.BigInt类型

BigInt是大于2^53-1的值

const num1 = Number.MAX_SAFE_INTEGER + 1;
const num2 = Number.MAX_SAFE_INTEGER + 2;
console.log(num1 == num2); // true

let max: bigint = BigInt(Number.MAX_SAFE_INTEGER);
console.log(max + BigInt(1) === max + BigInt(2)); // false

number类型和bigInt类型是不兼容的。

四、类型断言


1.联合类型

// 普通联合类型
let numOrStr: string | number;
let el: HTMLElement | null = document.getElementById('id');

// 字面量类型
// 类型的内容是固定的。如果类型很复杂,我们希望后续复用,可以把类型单独提取出来。
type IType = 'a' | 'b' | 'c';
let type1: IType = 'b';
let type2: IType = 'c';

2.非空断言!

let el: HTMLElement | null = document.getElementById('id');
el!.innerHTML = 'xxx';

3.强制转化类型 as | <>

强转类型,必须保证联合类型中有才可以。

let xx = string | number | undefined;
(xx as string).split('');
(<string>xx).split(''); // 和jsx冲突,不建议使用

4.双重断言

(xx as any) as boolean;

五、函数类型


1.函数的两种声明方式

// function 关键字,函数声明
function sum(a: string, b: string):string {
    return a+b;
}
sum('a','b')

// 函数表达式
type Sum = (a1: string, b1: string) => string;
let sum: Sum = (a: string, b: string) => {
    return a + b;
};

函数重载只能用函数声明的写法。使用类型别名只能用函数表达式的写法。

2.可选参数

let sum = (a: string, b?: string):string => {
    return a + b;
};
sum('a'); // 可选参数必须在其他参数的最后面

3.默认参数

let sum = (a: string, b: string = 'b'): string => {
    return a + b;
};
sum('a'); // 默认参数必须在其他参数的最后面

4.剩余参数

const sum = (...args: string[]): string => {
    return args.reduce((memo, current) => memo += current, '')
}
sum('a', 'b', 'c', 'd');

5.函数重载

function toArray(value: number): number[]
function toArray(value: string): string[]
function toArray(value: number | string) {
    if (typeof value == 'string') {
        return value.split('');
    } else {
        return value.toString().split('').map(item => Number(item));
    }
}
toArray(123); // 根据传入不同类型的数据 返回不同的结果
toArray('123');

六、类


1.ts中定义类

class Pointer{
    x: number = 1; // 声明的变量会被增加到实例上,实例上的属性必须先声明
    y!: number;
    constructor(x:number, y?:number, ...args:number[]){
        this.x = x;
        this.y = y as number;
    }
}
let p = new Pointer(100,200);

实例上属性必须先声明再使用。构造函数中的参数可以使用可选和剩余参数。

2.类中的修饰符

  • pubic 修饰符 自己、子类、实例均可以访问到。
class Animal {
    constructor(public name: string, public age: number) {
    }
}
class Cat extends Animal {
    constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name,this.age); // 子类访问  Tom 18
    }
}
let p = new Cat('Tom', 18);
console.log(p.name,p.age); // 外层访问  Tom 18
  • protected 修饰符 自己和子类可以访问到,实例无法访问。
class Animal {
    constructor(protected name: string, protected age: number) {
        this.name = name;
        this.age = age;
    }
}
class Cat extends Animal {
    constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name, this.age)
    }
}
let p = new Cat('Tom', 18);
console.log(p.name,p.age);// 无法访问
  • private 修饰符 只有自己可以访问到,子类和实例均无法访问。
class Animal {
    constructor(private name: string, private age: number) {
        this.name = name;
        this.age = age;
    }
}
class Cat extends Animal {
    constructor(name: string, age: number) {
        super(name, age);
        console.log(this.name, this.age); // 无法访问
    }
}
let p = new Cat('Tom', 18); 
console.log(p.name,p.age);// 无法访问
  • readonly 只读修饰符 只有自己可以访问到
class Animal {
    constructor(public readonly name: string, public age: number) {
        this.name = name;
        this.age = age;
    }
    changeName(name:string){
        this.name = name; // 仅读属性只能在constructor中被赋值
    }
}
class Cat extends Animal {
    constructor(name: string, age: number) {
        super(name, age);
    }
}
let p = new Cat('Tom', 18); 
p.changeName('Jerry');

3.静态属性和方法、属性访问器

class Animal {
    static type = '哺乳动物'; // 静态属性
    static getName() { // 静态方法
        return '动物类';
    }
    private _name: string = 'Tom';

    get name() { // 属性访问器
        return this._name;
    }
    set name(name: string) {
        this._name = name;
    }
}
let animal = new Animal();
console.log(animal.name); // Tom
console.log(Animal.getName()); // 动物类
console.log(Animal.type); // '哺乳动物'

静态属性和方法是可以被子类继承的

4.super属性

class Animal {
    say(message:string){
        console.log(message);
    } 
    static getType(){
        return '动物'
    }
}
class Cat extends Animal {
    say(){ // 原型方法中的super指代的是父类的原型
        super.say('猫猫叫');
    }
    static getType(){ // 静态方法中的super指代的是父类
        return super.getType()
    }
}
let cat = new Cat();
console.log(Cat.getType()) // 动物
console.log(cat.say()); // 猫猫叫

5.类的装饰器

装饰器,就是一个函数,不要把他想的很复杂。

1)装饰类
function addSay(target:any){
    target.prototype.say = function(){console.log('say')}
}
@addSay
class Person {
    say!:Function
}
let person = new Person
person.say(); // say

装饰类可以给类扩展功能,需要开启experimentalDecorators:true

2)装饰类中的属性
// target => Person.prototype
function toUpperCase(target:any,key:string){
    console.log(target, key);
    let value = target[key]; 
    Object.defineProperty(target,key,{
        get(){
            return value.toUpperCase();
        },
        set(newValue){
            value = newValue
        }
    })
}
// target => Person
function double(target: any, key: string) {
    let value = target[key];
    Object.defineProperty(target, key, {
        get() {
            return value * 2;
        },
        set(newValue) {value = newValue}
    })
}
class Person {
    @toUpperCase
    name: string = 'XiaoTianShi'
    @double
    static age: number = 10;
    getName() {
        return this.name;
    }
}
let person = new Person();
console.log(person.getName(),Person.age) // XIAOTIANSHI 20

装饰属性可以对属性的内容进行改写。装饰的是实例属性则target指向类的原型,装饰的是静态属性,则target指向类本身。

3)装饰类中的方法
function get(params: any) {
    return function (target:any,key:string,descriptor:PropertyDescriptor) {
        console.log(descriptor);
        // { 
        //     value: [Function],
        //     writable: true,
        //     enumerable: true,
        //     configurable: true 
        // }

        // 修改类中定义的方法,把装饰器方法里面传入的所有参数改为string类型
        let oMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            args = args.map(val => {
                return String(val)
            });
            oMethod.apply(this, args);
        }
    }
}
class Person {
    name: string = 'TianShi'
    static age: number = 10;
    @get('方法入参')
    getName(...args: any[]) {
        console.log(args, this.name); // ['111', '111'] // TianShi
    }
}
let person = new Person();
console.log(person.getName(111, '111')); 

装饰方法,可以对方法进行重写。装饰的是实例方法则target指向类的原型,装饰的是静态方法,则target指向类本身。

4)装饰方法中的参数
function addPrefix(params: any){
    return function(target:any,key:string,paramIndex:number){
        console.log(target,key,paramIndex, params, params[paramIndex]); // Person.prototype  getName  0  uuid  u
        // 扩展属性
        target.expendProp = params;
    }
}
class Person {
    name: string = 'XiaoTianShi'
    prefix!:string
    getName(@addPrefix('uuid') prefix:string) {
        console.log(prefix); // -ts
        return this.name;
    }
}
let person: any = new Person();
console.log(person.getName('-ts')); // XiaoTianShi
console.log(person.expendProp); // uuid

参数装饰器表达式会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些原始数据。
接收3个参数:
1.对于静态成员来说是构造函数,对于实例成员是类的原型对象。
2.方法的名字。
3.参数咋函数参数列表中的索引。

5)装饰器执行顺序

属性装饰器 => 方法装饰器 => 参数装饰器2 => 参数装饰器1 => 类装饰器2 => 类装饰器1

function logClass1(params:string) {
    return function(target:any) {
        console.log('类装饰器1');
    }
}
function logClass2(params:string) {
    return function(target:any) {
        console.log('类装饰器2');
    }
}
function logAttribute(params?:string) {
    return function(target:any, attr:any) {
        console.log('属性装饰器');
    }
}
function logMethod(params?:string) {
    return function(target:any, methodName:any, desc: any) {
        console.log('方法装饰器');
    }
}
function logParams1(params?:string) {
    return function(target:any, methodName:any, paramsIndex: any) {
        console.log('参数装饰器1');
    }
}
function logParams2(params?:string) {
    return function(target:any, methodName:any, paramsIndex: any) {
        console.log('参数装饰器2');
    }
}

@logClass1('hello')
@logClass2('world')
class HttpClient {
    @logAttribute()
    public apiUrl: string | undefined;
    constructor() {}

    @logMethod()
    getData() {}

    setData(@logParams1() param1:any, @logParams2() param2:any) {

    }
}

let p:any = new HttpClient();

6.抽象类

抽象类无法被实例化,只能被继承。抽象方法必能在抽象类中实现,只能在抽象类的具体子类中实现,而且,必须实现。

abstract class Animal{
    name!:string;
    abstract speak():void
}
class Cat extends Animal {
    speak(){
        console.log('猫猫叫');
    }
}
class Dog extends Animal{
    speak():string{
        console.log('汪汪叫');
        return 'wangwang'
    }
}

定义类型时void表示函数的返回值为空(不关心返回值类型)。

七、接口


接口,interface描述对象的形状和结构,方便复用。

1.函数接口参数

interface IPerson {
    name:string;
    age:number;
}
type IPerson = {name: string, age: number} | string;
const people = (obj:IPerson):string =>{
    return `${obj.name}今年${obj.age}岁!`
}

people('xiaotianshi', 18)

通过接口描述和约束了函数的参数。 interface可以被类实现和继承,type不可以。 type可以用联合类型,interface写联合类型比较恶心。

2.函数类型接口

interface ISum {
    (a: string, b: string): string;
}
const sum: ISum = (a, b) => {
    return a + b;
}
sum('1', '2');

通过接口限制函数的参数类型和返回值类型。

3.函数混合类型

给函数定义一个属性,我们应该怎么描述它的类型呢?使用一个计数器的例子来看下,每次调用就+1。

interface ICounter {
    (): number; // 限制函数类型
    count: 0 // 限制函数上的属性
}
let fn: ICounter = (() => {
    return fn.count++;
}) as ICounter;
fn.count = 0;
console.log(fn()); // 0
console.log(fn()); // 1

4.对象接口

// 只读属性
interface IVegetables {
    readonly color: string,
    size: string
}
// 可选属性,联合类型
interface IVegetables {
    price?: number,
    taste: 'sour' | 'sweet'
}
// 任意属性
interface IVegetables {
    [key: string]: any;
}
const tomato: IVegetables = {
    color: 'red',
    size: '10',
    taste: 'sour',
    shape: 'circle'
}
// 多余的属性除了任意属性外,也可以使用类型断言
const tomato: IVegetables = {
    color: 'red',
    size: '10',
    taste: 'sour',
    type: '蔬菜'
} as IVegetables;
// 仅读属性不能进行修改
tomato.color = 'green'; 

?表示可选属性,readonly标识的是只读属性,不可以修改。 相同命名的接口将会合并,会改变原有接口。

5.任意属性,可索引接口

interface Person {
    name: string;
    [key: string]: any
}
let p: Person = {
    name: 'xiaotianshi',
    age: 18,
    [Symbol()]: 'high'
}

任意属性可以对一部分属性做限制,其余的属性随意增减。

interface IArr {
    [key: number]: any
}
let p: IArr = {
    0: '1', 1: '2', 3: '3'
}
let arr: IArr = [1, 'd', 'c'];

可索引接口用于标识数组。

6.类接口

interface Speakable {
    name: string;
    speak(): void;
}
interface ChineseSpeakable {
    speakChinese(): void
}
class Speak implements Speakable, ChineseSpeakable {
    name!: string
    speak() {}
    speakChinese() {}
}

一个类可以实现多个接口,类中必须要实现接口中的属性和方法

7.接口继承

interface Speakable {
    speak(): void
}
interface SpeakChinese extends Speakable {
    speakChinese(): void
}
class Speak implements SpeakChinese {
    speakChinese(): void {
        throw new Error("Method not implemented.");
    }
    speak(): void {
        throw new Error("Method not implemented.");
    }
}

8.构造函数类型

类可以充当类型,描述实例。

// type IPerson = new (name: string) => Person;
interface IPerson<T> {
    new (name: string): T
}
function createInstance<T>(clazz: IPerson<T>, name: string) {
    return new clazz(name);
}

class Person {
    age: 18;
    constructor(public name: string){}
}
class Dog {
    color: 'yellow';
    constructor(public name: string) {}
}

let r = createInstance<Dog>(Dog, '小狗'); // r的类型是Dog,可以.出来color和name

typeinterface的不同写法。new() 表示当前是一个构造函数类型。 为啥用到了泛型呢?因为返回值类型不能确定,我们不知道是Dog还是Person。 类的类型是用来描述实例的。

八、泛型


通常是执行时才能确定类型的时候,需要用到泛型。通常在函数、类、接口、别名中使用。

1.指定函数参数类型

  • 单个泛型
const getArray = <T>(times: number, val: T): T[] => {
    let result: T[] = [];
    for (let i = 0; i < times; i++) {
        result.push(val);
    }
    return result;
}
getArray(3, 3); // 3 => T => number
  • 多个泛型
// 元组交换类型
function swap<T, K>(tuple: [T, K]): [K, T] {
    return [tuple[1], tuple[0]]
}
console.log(swap(['a','b']))

2.函数标注的方式

  • 类型别名
type TArray = <T, K>(tuple: [T, K]) => [K, T];
const getArray:TArray = <T, K>(tuple: [T, K]): [K, T] => {
    return [tuple[1], tuple[0]]
}
  • 接口
interface IArray{
    <T,K>(typle:[T,K]):[K,T]
}
const getArray:IArray = <T, K>(tuple: [T, K]): [K, T] => {
    return [tuple[1], tuple[0]]
}

type多与keyof作为联合类型使用,interface可以被继承和实现,扩展性更好。

3.泛型接口使用

interface ICreateArray<T> { // 这里的T是使用接口的时候传入
    <U>(a: T, b: U): U[] // 这里的U是调用函数的时候传入
}
let createArray: ICreateArray<number> = <U>(times: number, value: U): U[] => {
    let result = [];
    for (let i = 0; i < times; i++) {
        result.push(value);
    }
    return result;
}
createArray(1, 'xxx');

泛型传入地方不同,调用时机也不同。放在函数前,表示调用函数时确定类型,放在接口后面,表示使用接口的时候确定类型。

4.默认泛型

interface T2<T = string> {
    name: T
}
type T22 = T2;
let p: T22 = { name: 'tianshi' } // p的类型是T2

默认泛型和函数默认参数类似,用于指定泛型的默认类型。

5.类中的泛型

  • 创建实例时,提供类型
class MyArray<T>{ // T => number
    arr: T[] = [];
    add(num: T) {
        this.arr.push(num);
    }
    getMaxNum(): T {
        let arr = this.arr
        let max = arr[0];
        for (let i = 1; i < arr.length; i++) {
            let current = arr[i];
            current > max ? max = current : null
        }
        return max;
    }
}
let myArr = new MyArray<number>();
myArr.add(3);
myArr.add(1);
myArr.add(2);
console.log(myArr.getMaxNum());
  • 构造函数类型
interface IPerson<T> {
    new (name: string, age: number): T
}

const createClass = <T>(clazz: IPerson<T>, name: string, age: number) => {
    return new clazz(name, age);
}

class Person {
    constructor(public name: string, public age: number) { }
}

let r = createClass<Person>(Person, 'xxx', 18);

6.泛型约束

  • 属性约束
interface IWithLength {
    length:number
}
const computeArrayLength = <T extends IWithLength, K extends IWithLength>(arr1: T, arr2: K): number => {
    return arr1.length + arr2.length;
}
computeArrayLength('123', {length: 3});
computeArrayLength([1, 2], '34');

必须都有length属性才行。

const sum = <T extends number>(a: T, b: T): T => {
    return (a + b) as T
}
let r = sum<number>(1, 2); 

类型必须是number才行。

  • 对象约束
const getVal = <T extends object, K extends keyof T>(obj: T, key: K): T[K] => {
    return obj[key];
}
getVal({a: 1, b: 2}, 'b');

第二个参数,必须是第一个参数中的某个键。

九、兼容性


这里的兼容性和我们浏览器的兼容性不同,ts的兼容性主要是看结构是否兼容,核心是考虑安全性

1.基本数据类型的兼容性

let temp: string | number;
let num!: number;
temp = num;

temp中有num的类型,所以可以兼容,可以赋值。

let num: {
    toString(): string
}
let str: string = 'tianshi';
num = str;

字符串中具备toString()方法,所以可以进行兼容,可以赋值。

2.接口的兼容性

interface IAnimal {
    name: string,
    age: number
}
interface IPerson {
    name: string,
    age: number,
    address: string
}
let animal: IAnimal;
let person: IPerson = {
    name: 'will',
    age: 18,
    address: '海淀'
};
animal = person;

接口的兼容性,只要满足接口中所需要的类型即可。人是动物,但是动物不一定是人。

3.函数的兼容性

函数的兼容性,多是比较参数和返回值。

  • 参数
let sum1 = (a: string, b: string) => a + b;
let sum2 = (a: string) => a;
sum1 = sum2

可以传递2个,但是你传递了1个,是安全的。
只能传递1个参数,但是你给了2个,不安全。

type Func<T> = (item: T, index: number) => void
function forEach<T>(arr: T[], cb: Func<T>) {
    for (let i = 0; i < arr.length; i++) {
        cb(arr[i], i); // item和index都返回了
    }
}
forEach([1, 2, 3], (item) => { // 用到了item,没有用index,不传index也可以
    console.log(item);
});

赋值函数的参数要小于等于被赋值的函数,与对象相反。

  • 返回值
type sum1 = () => string | number
type sum2 = () => string;

let fn1!: sum1;
let fn2!: sum2;
fn1 = fn2;
type sum1 = () => { name: string };
type sum2 = () => { name: string; age: number };

let fn1!: sum1;
let fn2!: sum2;
fn1 = fn2;

4.函数的逆变协变

函数的参数是逆变的,可以传递父类。函数的返回值是协变的,可以返回子类。(在非严格模式下,函数的参数是双向协变的)。

class Parent {
    address: string = '中国'
}
class Child extends Parent {
    money: number = 100
}
class Grandsom extends Child {
    name: string = '小明';
}
type Callback = (person: Child) => Child
function execCallback(cb: Callback) { }
// 传父,返孙
let fn = (person: Parent) => new Grandsom;
execCallback(fn);

参数,接受Child,传入Parent。返回值,本来返回Child,可以返回Grandsom。缩小入参返回,扩大返回值范围。目的是为了安全性。

以上,是对类型的兼容性的阐述,告诉你类型可以这么用。从应用层面说,实际应用的时候,入参需要Child,就传Child,传Parent会报错。

5.类的兼容性

class Perent {
    name: string = 'tianshi';
    age: number = 18
}
class Parent1 {
    name: string = 'tianshi';
    age: number = 18
}
let parent: Perent = new Parent1;

注意:只要有private或者protected修饰符,类型就会不一致。但是继承的类可以兼容。

class Parent {
    protected name: string = 'tianshi';
    age: number = 18
}
class Child extends Parent { };
let child: Parent = new Child;

6.泛型的兼容性

interface MyType<T> { }
let obj1: MyType<string>;
let obj2: MyType<number>;
obj1 = obj2;

7.枚举的兼容性

enum USER1 {
    role = 1
}
enum USER2 {
    role = 1
}
let user1!: USER1
let user2!: USER2
user1 = user2 // 错误语法

不同的枚举类型不兼容。

十、类型保护


所谓类型保护,就是判断执行的代码片段,自动识别出变量属性和方法。

1.typeof 类型保护

function double(val: number | string) {
    if (typeof val === 'number') {
        val. // 可以点出来数字类型的属性和方法
    } else {
        val. // 可以点出来字符串类型的属性和方法
    }
}

2.instanceof类型保护

class Cat { eat(){} }
class Dog { drink(){} }

const getInstance = (clazz: { new(): Cat | Dog }) => {
    return new clazz();
}
let r = getInstance(Cat);
if (r instanceof Cat) {
    r // 猫
} else {
    r // 狗
}

3.in类型保护

interface Fish {
    swiming: string,
}
interface Bird {
    fly: string,
    leg: number
}
function getType(animal: Fish | Bird) {
    if ('swiming' in animal) {
        animal // Fish
    } else {
        animal // Bird
    }
}

4.可识别联合类型

通过增加一个唯一的自定义属性,来识别类型。

增加一个class属性,通过该属性判断是用的WarningButton类型,还是DangerButton类型。

interface WarningButton {
    class: 'warning'
}
interface DangerButton {
    class: 'danger'
}
function createButton(button: WarningButton | DangerButton) {
    if (button.class == 'warning') {
        button // WarningButton
    } else {
        button // DangerButton
    }
}

5.null保护?? ||

const addPrefix = (num?: number) => {
    num = num || 1.1; // 确保外层取值是有真值的
    let out = num.toFixed(); // 外层作用域不需要使用可选链,因为 || 已经确保有值。
    function prefix(fix: string) {
        return fix + num?.toFixed() // ?.内层作用域还是需要使用可选链判断
    }
    return prefix('zxd');
}
console.log(addPrefix());

注意:ts无法检测内部函数变量类型。

6.自定义类型保护is

interface Fish {
    swiming: string,
}
interface Bird {
    fly: string,
    leg: number
}
function isFish(animal: Fish | Bird): animal is Fish {
    return 'swiming' in animal
}
function getAniaml(animal: Fish | Bird) {
    if (isFish(animal)) {
        animal // 可以点出来鱼的属性
    } else {
        animal // 可以点出来鸟的属性
    }
}
getAniaml({ swiming: 'swim' });
function isString(val: any): val is string {
    return Object.prototype.toString.call(val) === '[object String]';
}

let str = '123';
if (isString(str)) {
    str
}

is关键字,它被称为类型谓词,用来判断一个变量是否属于某个接口或类型。如果需要封装一个类型判断函数,应该第一时间想到它。 animal is Fish, val is string返回是一个boolean值,即表示返回值是一个布尔值。

7.完整性保护

完整性保护,保证代码逻辑全部都覆盖到。主要靠never,利用never无法到达最终结果的特性,来保证代码的完整性。

interface ICircle {
    kind: 'circle',
    r: number
}
interface IRant {
    kind: 'rant',
    width: number,
    height: number
}
interface ISquare {
    kind: 'square',
    width: number
}
type Area = ICircle | IRant | ISquare
const isAssertion = (obj: never) => { }
const getArea = (obj: Area) => {
    switch (obj.kind) {
        case 'circle':
            return 3.14 * obj.r ** 2
+       case 'rant':
+           return obj.width * obj.height
+       case 'square':
+           return obj.width * obj.width
        default:
            return isAssertion(obj); // 必须实现所有逻辑
    }
}
getArea({kind: 'square', width: 100});

必须实现所有逻辑才可以。

十一、类型推断

ts有非常棒的类型推断能力,不需要我们定义用到的所有类型。

1.赋值推断

let str = 'tianshi';
let age = 18;
let boolean = true;

赋值时推断,会根据赋值推断出变量类型。

2.返回值推断

自动推断函数返回值类型。

function sum(a: string, b: string) {
    return a + b;
}
let r = sum('a', 'b');

3.函数推断

从左到右,依次推断参数类型,返回值类型。

type Sum = (a: string, b: string) => string;
const sum: Sum = (a, b) => a + b;

4.属性推断

通过属性值,推断出属性的类型。

let person = {
    name: 'tianshi',
    age: 18
}
let { name, age } = person;

5.类型反推

使用typeof关键字反推变量类型。

let person = {
    name: 'tianshi',
    age: 18
}
type Person = typeof person; // {name: string, age: number}

6.索引访问操作符

通过[]访问

interface IPerson {
   name: string,
   age: number,
   job: {
       address: string
   }
}
type IJob = IPerson['job']['address']; // type IJob = string;

注意:只能通过[]访问接口的类型,.无效。

7.类型映射

interface IPerson {
   name: string,
   age: number
}
type IMapPerson = { [key in keyof IPerson]: IPerson[key] } // type IMapPerson = { name: string, age: number };

keyof关键字,用来获取到接口定义的键,同js中对象的Object.keys()类似。 [key in keyof IPerson]: IPerson[key]标识遍历IPerson的key,并赋值类型。

十二、交叉类型

交叉类型(Intersection Type)是将多个类型合并为一个类型。在原有的类型基础上想去扩展属性,可以用交叉类型。

通常,我们用&符用来实现交叉类型。

interface Person1 {
    handsome: string,
}
interface Person2 {
    high: string,
}
type P1P2 = Person1 & Person2;
let p: P1P2 = { handsome: '帅', high: '高' };

交叉类型类似于数学中的交集,比如两拨人,一拨很高,一拨很帅,我们要找出交叉部分,又高又帅的人。

function mixin<T, K>(a: T, b: K): T & K {
    return { ...a, ...b }
}
const newType = mixin({ name: 'tianshi' }, { age: 18 }) 
// newType: { name: string } & { age: number }
interface IPerson1 {
    name: string,
    age: number
}
interface IPerson2 {
    name: number
    age: number
}
type person = IPerson1 & IPerson2
let name!: never
let person: person = { name, age: 11 };

两个属性之间 string & number 的值为 never。因为不可能存在是字符串同时又是数字类型的属性。 ts核心是为了安全,交叉类型可以赋予给没有交叉前的类型。

十三、条件类型


1.条件类型基本使用

使用extends和三元表达式,实现条件判断

interface Fish {
    name1: string
}
interface Water {
    name2: string
}
interface Bird {
    name3: string
}
interface Sky {
    name4: string
}
type Condition<T> = T extends Fish ? Water : Sky;
let con1: Condition<Fish> = { name2: '水' };

2.条件类型分发

let c: Condition<Fish | Bird> = { name2: '水' };

// 类型会依次进行分发,最终采用联合类型作为结果。等价于:
type c1 = Condition<Fish>;
type c2 = Condition<Bird>;
type c = c1 | c2

一个例子:当传入name属性时,age属性必传。当不传name属性时,age属性可选。

interface ISchool1 {
    name: string;
    age: number
}
interface ISchool2 {
    age?: number;
    size: string;
}

type School<T> = T extends { name: string } ? ISchool1 : ISchool2;
let school: School<{name: 'xxx'}>  // let school: ISchool1

3.内置条件类型

  • Exclude排除类型
type Exclude<T, U> = T extends U ? never : T;
type MyExclude = Exclude<'1' | '2' | '3', '1' | '2'>  // MyExclude = '3'
  • Extract抽取类型
type Extract<T, U> = T extends U ? T : never;
type MyExtract = Extract<'1' | '2' | '3', '1' | '2'> // MyExtract = "1" | "2"
  • NonNullable非空检测
type NonNullable<T> = T extends null | undefined ? never : T
type MyNone = NonNullable<'a' | null | undefined> // MyNone = 'a'

4.infer类型推断

infer表示,在extends条件语句中,待推断的类型变量。(只能用在extends条件语句中)

  • ReturnType返回值类型
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;

在这个条件语句中,infer R表示待推断的函数返回值。

整句表示:如果T能赋值给(...args: any) => infer R,则结果是(...args: any) => infer R类型中的返回值R,否则返回never

type ReturnType<T> = T extends (...args: any) => infer R ? R : never
function getUser(name: string, age: number) {
    return { name: 'tianshi', age: 18 }
}
type MyReturn = ReturnType<typeof getUser> // MyReturn = { name: string, age: number };
type MyString = ReturnType<string> // MyString = never
  • Parameters参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : any;

在这个条件语句中,infer R表示待推断的参数类型。

整句表示:如果T能赋值给(...args: infer P) => any,则结果是(...args: infer P) => any类型中的参数P,否则返回any

function getUser(name: string, age: number) {
    return { name: 'tianshi', age: 18 };
}
type MyParams = Parameters<typeof getUser>; // MyParams = [string, number]
type MyString = Parameters<string>; // MyString = any
  • ConstructorParameters构造函数参数类型
type ConstructorParameters<T> = T extends { new (...args: infer P): any } ? P : never;

在这个条件语句中,infer P表示待推断的构造函数参数类型。

整句表示:如果T能赋值给new (...args: infer P): any,则结果是new (...args: infer P): any类型中构造函数参数P,否则返回never

class Person {
    constructor(name: string, age: number) { }
}
type MyConstructor = ConstructorParameters<typeof Person> // MyConstructor = [string, number]
type MyString = ConstructorParameters<string> // MyString = never
  • InstanceType实例类型
type InstanceType<T> = T extends { new(...args: any): infer R } ? R : any;

在这个条件语句中,infer R表示待推断的实例类型。

整句表示:如果T能赋值给new(...args: any): infer R,则结果是new(...args: any): infer R类型中构造函数参数R,否则返回any

class Person {
    constructor(name: string, age: number) { }
  }
type MyInstance = InstanceType<typeof Person>; // MyInstance = Person
type MyString = InstanceType<string>; // MyString = any

5.infer实践

  • 将数组类型转化为联合类型
type ElementOf<T> = T extends Array<infer E> ? E : never;

在这个条件语句中,infer E表示待推断的数组元素类型。

整句表示:如果T能赋值给Array<infer E>,则结果返回Array<infer E>数组每项的类型,否则返回never

type TupleToUnion = ElementOf<[string, number, boolean]>;  // TupleToUnion = string | number | boolean;
type TupleString = ElementOf<string>;  // TupleString = never;
  • 将两个函数的参数转化为交叉类型
type ToIntersection<T> = T extends ([(x: infer U) => any, (x: infer U) => any]) ? U : never;

在这个条件语句中,infer U表示待推断的函数参数类型。

整句表示:要把T1T2赋予给x,那么x的值就是T1T2的交集。

type T1 = { name: string };
type T2 = { age: number };
type t3 = ToIntersection<[(x: T1) => any, (x: T2) => any]>  // t3 = T1 & T2
type t4 = ToIntersection<[string, string]> // t4 = never

十四、内置类型

1.partial转化可选属性

type Partial<T> = { [K in keyof T]?: T[K] }

原理就是遍历所有属性,并设置为可选属性。

interface Company {
    num: number
}
interface Person {
    name: string,
    age: string,
    company: Company
}
type PartialPerson = Partial<Person>; // { name: string, age: string, company: Company }

注意:无法实现深度转化。

实现一个深度转化,如果值是对象,则继续深度转化

type DeepPartial<T> = {
    [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
type DeepPartialPerson = DeepPartial<Person>;

2.Required转化必填属性

type Required<T> = {[K in keyof T]-?:T[K]}

原理同Paritial相同,Partial是将必填转化为选填,Required使用-?将选填转为必填。

interface Company {
    num: number
}
interface Person {
    name: string,
    age: string,
    company: Company
}
type PartialPerson = Partial<Person>;
type RequiredPerson = Required<PartialPerson>

3.Readonly转化仅读属性

type Readonly<T> = { readonly [K in keyof T]: T[K] }

实现原理是给每个属性增加readonly修饰符,转为可读属性。

interface Company {
    num: number
}
interface Person {
    name: string,
    age: string,
    company: Company
}
type RequiredPerson = Readonly<Person>

4.Pick在已有类型中挑选所需属性

type Pick<T, K extends keyof T> = { [P in K]: T[P] }

分析一下,T表示传入的对象类型,K extends keyof T表示约束K的类型,必须是T的属性。[P in K]: T[P]表示从T中挑选出属于K的属性及类型并返回。

interface Company {
    num: number
}
interface Person {
    name: string,
    age: string,
    company: Company
}
type PickPerson = Pick<Person, 'name' | 'age'> // pickPerson = { name: string, age: string }

5.Record记录类型

type Record<K extends keyof any, T> = { [P in K]: T }

分析一下,K extends keyof any泛型约束,表示K的类型是string, number, symbol中的任意一种。(可以作为对象的键的类型)

[P in K]: T表示键是string, number, symbol中的任意一种,值是传入的泛型T

let person: Record<string, any> = { name: 'tianshi', age: 18 };

我们经常用record类型表示映射类型。

function map<T extends keyof any, K, U>(obj: Record<T, K>, callback: (item: K, key: T) => U): Record<T, U> {
    let result = {} as Record<T, U>
    for (let key in obj) {
        result[key] = callback(obj[key], key)
    }
    return result
}

const r = map({ name: 'xiaotianshi', age: 18 }, (item, key) => {
    return item
});
// r = Record<"name" | "age", string | number>

逐句分析:

  1. <T extends keyof any, K, U>三个类型形参,T被约束为string, number, symbol类型中的一种。
  2. obj: Record<T, K>定义obj对象的键是T类型,值是K类型。
  3. callback: (key: T, value: K) => U定义callback的类型,入参key的类型是T,value的类型是K,返回值类型是U
  4. Record<T, U>定义返回的对象类型,键是T类型,值是U类型。

本例中,通过入参{ name: 'xiaotianshi', age: 18 }可以推断出T是类型 stringKstring | number。通过let result = {} as Record<T, U>可以推断出U的类型是callback函数的返回值的类型,其实和K类型一样,也是string | number

6.Omit忽略属性

type Exclude<T, U> = T extends U ? never : T;
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

分析一下:
Exclude<keyof T, K>表示从keyof T中排除掉K
Pick<T, Exclude<keyof T, K>>然后从T中挑选出刚才排除掉K剩下的属性。

let person = {
    name: 'tianshi',
    age: 18,
    address: '海淀'
}
type OmitAddress = Omit<typeof person, 'address'> // type OmitAddress = { name: string; age: number; }

忽略person中的address属性,先排除到address属性,然后在person中挑选出需要的属性。

十五、装包和拆包


1.装包

type Proxy<T> = {
    get(): T,
    set(value: T): void
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>
}
let props = {
    name: 'tianshi',
    age: 18
}
function proxify<T>(obj: T): Proxify<T> {
    let result = {} as Proxify<T>;
    for (let key in obj) {
        let value = obj[key];
        result[key] = {
            get() {
                return value
            },
            set: (newValue) => value = newValue
        }
    }
    return result
}
let proxyProps = proxify(props);
// let proxyProps: Proxify<{ name: string; age: number; }>

2.拆包

function unProxify<T>(proxyProps: Proxify<T>): T {
    let result = {} as T;
    for (let key in proxyProps) {
        let value = proxyProps[key];
        result[key] = value.get()
    }
    return result
}
let proxy = unProxify(proxyProps);
// let proxy: { name: string; age: number; }

十六、自定义类型


1.Diff实现

let person1 = {
    name: 'tianshi',
    age: 18,
    address: '中国'
}
let person2 = {
    address: '中国',
}
type Diff<T extends object, K extends object> = Omit<T, keyof K>
type DiffPerson = Diff<typeof person1, typeof person2>
// type DiffPerson = { name: string; age: number; }

2.InterSection交集

let person1 = {
    name: 'tianshi',
    age: 18,
    address: '中国'
}
let person2 = {
    address: '中国',
}
type InterSection<T extends object, K extends object> = Pick<T, Extract<keyof T, keyof K>>
type InterSectionPerson = InterSection<typeof person1, typeof person2>
// type InterSectionPerson = { address: string; }

3.Overwrite属性覆盖

如果存在已有属性则使用新属性类型进行覆盖操作

type OldProps = { name: string, age: number, visible: boolean };
type NewProps = { age: string, other: string };

type Diff<T extends object, K extends Object> = Omit<T, keyof K>
type InterSection<T extends object, K extends object> = Pick<T, Extract<keyof T, keyof K>>
type Overwrite<T extends object, K extends object, I = Diff<T, K> & InterSection<K, T>> = Pick<I, keyof I>
type ReplaceProps = Overwrite<OldProps, NewProps>
// type ReplaceProps = { name: string; age: string; visible: boolean; }

4.Merge对象合并

将两个对象类型进行合并操作。

type OldProps = { name: string, age: number, visible: boolean };
type NewProps = { age: string, other: string };

type Compute<A extends any> = { [K in keyof A]: A[K] };
type Merge<T, K> = Compute<Omit<T, keyof K> & K>;
type MergeObj = Merge<OldProps,NewProps>
// type MergeObj = { name: string; visible: boolean; age: string; other: string; }

十七、模块和命名空间


默认情况下,我们编写的代码处于全局命名空间下。

1.模块

文件模块:在你的TypeScript文件的根级别位置还有import或者export,那么会在这个文件中创建一个本地的作用域。

// a.ts导出
export default 'a';

// index.ts导入
import name from './index';

2.命名空间namespace

命名空间可以用于组织代码,避免文件内命名冲突。

  • 命名空间的使用
export namespace zoo {
    export class Dog { eat() { console.log('zoo dog'); } }
}
export namespace home {
    export class Dog { eat() { console.log('home dog'); } }
}

let dog_of_zoo = new zoo.Dog();
dog_of_zoo.eat();
let dog_of_home = new home.Dog();
dog_of_home.eat();
  • 命名空间嵌套使用
export namespace zoo {
    export class Dog { eat() { console.log('zoo dog'); } }
    export namespace bear{
        export const name = '狗熊'
    } 
}
console.log(zoo.bear.name);  // 狗熊

命名空间中导出的变量,可以通过命名空间使用。

十八、类型声明


1.声明全局变量

1) 普通类型声明
declare let age: number;
declare function sum(a: string, b: string): void;
declare class Animal { };
declare const enum Seaons {
    Spring,
    Summer,
    Autumn,
    Winter
}
declare interface Person {
    name: string,
    age: number
}

类型声明在编译时会被删除,不会影响真正代码执行。

  • 一个例子,声明jQuery类型 jQuery通过外部CDN方式引入,想在代码中直接使用。
declare const $: (selector: string) => {
    height(num?: number): void
    width(num?: number): void
};
$('').height();
2) 命名空间声明
declare namespace jQuery {
    function ajax(url: string, options: object): void;
    namespace fn {
        function extend(obj: object): void
    }
}
jQuery.ajax('/', {});
jQuery.fn.extend({});

namespace表示一个全局变量包含很多子属性,命名空间内部不需要使用 declare声明属性或方法。

2.类型声明文件

类型声明文件以.d.ts结尾,默认在项目编译时会查找所有以.d.ts结尾的文件。

// jquery.d.ts
declare const $: (selector: string) => {
    height(num?: number): void
    width(num?: number): void
};

declare namespace jQuery {
    function ajax(url: string, otpions: object): void;
    namespace fn {
        function extend(obj: object): void
    }
}

3.编写第三方声明文件

配置tsconfig.json

"moduleResolution": "node",
"baseUrl": "./",
"paths": {
    "*": ["types/*"]
}
  • jquery声明文件
// types/jquery/index.d.ts

declare function jQuery(selector: string): HTMLElement;
declare namespace jQuery {
    function ajax(url: string): void
}
export = jQuery;
  • events模块声明文件 使用:
import { EventEmitter } from "events";
var e = new EventEmitter();
e.on('message', function (text) {
    console.log(text)
})
e.emit('message', 'hello');

声明文件:

export type Listener = (...args: any[]) => void;
export type Type = string | symbol

export class EventEmitter {
    static defaultMaxListeners: number;
    emit(type: Type, ...args: any[]): boolean;
    addListener(type: Type, listener: Listener): this;
    on(type: Type, listener: Listener): this;
    once(type: Type, listener: Listener): this;
}

4.模块导入导出

import $ from 'jquery'  // 只适用于 export default $
const $ = require('jquery'); // 没有声明文件可以直接使用 require语法
import * as $ from 'jquery'  // 为了支持 Commonjs规范 和 AMD规范 导出时采用export = jquery
import $ = require('jquery')  // export = jquery 在commonjs规范中使用

5.第三方声明文件

@types是一个约定的前缀,所有第三方声明的类型库都会带有这样的前缀。

npm install @types/jquery -S

查找规范:

  • node_modules/jquery/package.json 中的types字段
  • node_modules/jquery/index.d.ts
  • node_modules/@types/jquery/index.d.ts

十九、扩展全局变量类型


1.扩展局部变量

可以使用接口对已有类型进行扩展。

interface String {
    double(): string
}
// String.prototype.double = function () {
//     return this as string + this;
// }
let str = 'tianshi'; // 可以点出来double
interface Window {
    mynane: string
}
console.log(window.mynane)

2.模块内全局扩展

declare global {
    interface String {
        double(): string;
    }
    interface Window {
        myname: string
    }
}

3.声明合并

同一名称的两个独立声明最终会被合并成一个单一声明,合并后的声明拥有原先两个声明的特性。

1)同名接口合并
interface Animal {
    name: string
}
interface Animal {
    age: number
}
let a: Animal = { name: 'tianshi', age: 18 };
2)命名空间的合并
  • 扩展类 给类添加静态成员
class Form { }
namespace Form {
    export const type = 'form'
}
console.log(Form.type); // form
  • 扩展方法
function getName() { }
namespace getName {
    export const type = 'form'
}
console.log(getName.type); // form
  • 扩展枚举类型
enum Seasons {
    Spring = 'Spring',
    Summer = 'Summer'
}
namespace Seasons {
    export let Autum = 'Autum';
    export let Winter = 'Winter'
}
console.log(Seasons.Autum) // Autum
3)交叉类型合并
import { Store } from 'redux';
type StoreWithExt = Store & {
    ext: string
}
let store: StoreWithExt;

4.生成声明文件

配置tsconfig.jsontrue生成声明文件。

"declaration": true

参考资料