万字长文助你打好 TS 基础

这篇文章是笔者在学习 typescript 后利用工作之余的时间为小伙伴一字一码结合自己的实践经验写的学习总结, 为大家分享一下 typescript 的魅力, 大型真香警告!

TypeScript 有什么特点?

它是JavaScript的超级,他可以编译成纯JavaScript;

  1. 类型检查
    JavaScript会在编译的时候进行严格的编译检查,意味着你可以在编译的时候发现代码可能带来的隐患;
  2. 语言拓展
    会包括来着ES6和未来提案的特性(例如异步操作和装饰器),也会从其他语言借鉴某些特性,比如接口和抽象类
  3. 工具属性
    可以编译成JavaScript在任何浏览器和操作系统上运行,无需任何运行时的额外开销

为什么要用TypeScript,他有什么好处?

VSCode具有强大的自动补全,导航和重构, 这使得接口定义可以直接代替文档, 直接通过接口提示一目了然, 进而提高开发效率, 降低维护成本

强类型语言和弱类型语言

强类型语言: 不允许改变变量的数据类型,除非强类型装换;

弱类型语言: 变量可以被赋值为任何类型

静态类型语言与动态类型语言

静态类型语言: 在编译的阶段确定是有变量的类型

动态类型语言: 在执行阶段确定是有变量的类型

静态类型语言 动态类型语言
对类型极度严格 对类型非常宽松
立即发现错误 Bug可能会秃然而来
运行时性能好 运行时性能差
自文档化 可读性差

type 项目的创建

npm install

tsc --init

tsc index.js

数据类型的对比

ES6 TypeScript
Symbol,Number,String,Boolean,Undefined,Null,Object,Array,Function Symbol,Number,String,Boolean,Undefined,Null,Object,Array,Function,any,void,never,元组,枚举,高级类型

TypeScript 类型

相当于强类型语言中的类型声明,可以对变量 / 函数起到约束的作用.

语法: (变量 / 函数) : type

例子

Number

var num:number = 123;//数字类型

String

var str:string = 'String';//字符类型

Boolean

var bol:boolean = false;//布尔类型

Array

//数组  纯字符数组
var strArr:string[] = ['a','b'];
var strArr2:Array =  ['a','b'];

//联合类型数组 
var jointArr:Array = [1,'a',2,'b'];

元组: 元组是一种特殊的数组, 它限定了数组的个数和类型.

var tuple:Array = [1,'a'];
//在这里可以对数组进行一些方法操作,对数组进行填充或者移除,的是无法越界取值/设置
tuple.push(2333); // ok

console.log(tuple[2]) // error 越界了

Object

var obj:object = {name: '进击的切图仔',age: 18,sex: '男'};

//如果这样说明的话,则无法修改对象的属性,因为并未明确指定属性的类型,真是声明它是一个对象而已

// obj.name = '嘤嘤嘤'; //bad
//解决方法
var obj1:{name: string,age: number,sex: string} = {name: '进击的切图仔',age: 18,sex: '男'};

obj1.name = '嘤嘤嘤'; //great

Symbol

Symbol 是用来干嘛的?
它可以确保变量 / 属性的唯一性, 可以解决命名冲突或者对变量的保密;

var sym = Symbol();
var sym2:symbol = Symbol();

function

var fun = (a:number,b:number):boolean => !(a + b)%2;
/*
* a,b 参数类型为数值, 且函数返回值为布尔类型
*/

var fun1:(a:number,b:number) => boolean;
fun1 = (x,y) => !(a + b)%2;

ps: 关于函数的其他定义为了方便理解, 会在下面的接口定义中细讲, 请耐心品尝!

undefined / null

var un:undefined = undefined;
var nu:null = null;
// un = 1;//bad
//num = undefined;//ok
/*
* Tip: 一旦给变量的类型为 undefined / null 后则不能再赋予其他类型,
*     因为官方文档指明 undefined / null 是其他类型的子类型,就是说你可以把 null 和 undefined 赋值给 number 类型的变量。 
*     但是其他类型的变量可以对其赋予 null 或者 undefined, 
*     此外需要将 tsconfig.js 的 "scriptNullChecks" 设置为 false 即可
*/

any

//在 ts 中,如果没有给变量明确确定一个类型时,默认就是 any 类型,意味着它可以被定义为任何类型;

var _any;
    _any = 1;
    _any = '2';
    _any = () => {};//...

void

//某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void;

function warnUser(): void {
    console.log("This is my warning message");
}

//声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
var vo:void = null;

never

//never 类型是 TypeScript 中的底层类型。它自然被分配的一些例子:
//一个从来不会有返回值的函数(如:如果函数内含有 while(true) {});
//一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') },foo 的返回类型是 never);
//never 只能被赋值到了一个 never

var nev = ()=> {while(1){}};
// nev = 2; //bad
var nev2:never;
    never2 = nev;//ok
    
// void 与 never 的区别, 同为没有返回值
// void 表示的是函数没有返回值
// never 表示的是函数根本就没有返回值 (报错了 / 死循环)导致函数无法彻底执行完毕;

枚举

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。

枚举有哪些好处呢?

举个例子,就和你的手机通讯录一样, 虽然手机号码有的时候会改变,但是你只需要知道用户名就行,不用刻意记住他的手机号码.

enum NumberList {
    one,
    two,
    three
}

编译器编译后的结果:

var NumberList;
(function (NumberList) {
    //这里涉及的是反向映射
    NumberList[NumberList["one"] = 0] = "one";
    NumberList[NumberList["two"] = 1] = "two";
    NumberList[NumberList["three"] = 2] = "three";
})(NumberList || (NumberList = {}));

ts 中的枚举编译成 js 脚本后是一个 js 对象.

  • 数字枚举
enum Num {
    a,
    b,
    c
}

console.log(Num.a,Num.b,Num.c) // 0,1,2

在无赋值情况下, 枚举默认是数字枚举, 且从 0 开始递增.

  • 字符串枚举
enum Str{
    name = '进击的切图仔',
    sex = '男',
    age = '保密'
}

console.log(`我叫${Str.sex},是一个${Str.sex}孩子`); //我叫进击的切图仔,是一个男孩子

ts 编译后的 js:

var Str;
(function (Str) {
    Str["name"] = "\u8FDB\u51FB\u7684\u5207\u56FE\u4ED4";
    Str["sex"] = "\u7537";
    Str["age"] = "\u4FDD\u5BC6";
})(Str || (Str = {}));
console.log("\u6211\u53EB" + Str.sex + ",\u662F\u4E00\u4E2A" + Str.sex + "\u5B69\u5B50");

可以看出在 字符串枚举 中是没有反向映射的;

  • 异构枚举

何为异构枚举,异构枚举 就是各种类型的枚举混用在一块;

enum Heteroge{
    age = 18,
    name = '进击的切图仔',
    index = 0,
    huabei,
    a
}

ts 编译后的 js:

var Heteroge;
(function (Heteroge) {
    Heteroge[Heteroge["age"] = 18] = "age";
    Heteroge["name"] = "\u8FDB\u51FB\u7684\u5207\u56FE\u4ED4";
    Heteroge[Heteroge["index"] = 0] = "index";
    Heteroge[Heteroge["huabei"] = 1] = "huabei";
    Heteroge[Heteroge["a"] = 2] = "a";
})(Heteroge || (Heteroge = {}));

枚举成员: ts 中的枚举成员分为两种,分别是const members (常量枚举)computed members (计算枚举);

const members 有哪些?

  1. 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
  2. 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
  3. 带括号的常量枚举表达式
  4. 一元运算符 +, -, ~其中之一应用在了常量枚举表达式
  5. 常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。 若常数枚举表达式求值后为 NaN或 Infinity,则会在编译阶段报错。
enum People{
    name = '进击的切图仔',
    age = 18,
}

enum ConstMembers{
    age = 6 + 6 + 6,
    name = People.name,
    sumsum = People.age * People.age,
    numnum = ~1,
    fixedNumNum = 3.14 | 0
}

编译后的 js:

var People;
(function (People) {
    People["name"] = "\u8FDB\u51FB\u7684\u5207\u56FE\u4ED4";
    People[People["age"] = 18] = "age";
})(People || (People = {}));

var ConstMembers;
(function (ConstMembers) {
    ConstMembers[ConstMembers["age"] = 18] = "age";
    ConstMembers["name"] = "\u8FDB\u51FB\u7684\u5207\u56FE\u4ED4";
    ConstMembers[ConstMembers["sumsum"] = 324] = "sumsum";
    ConstMembers[ConstMembers["numnum"] = -2] = "numnum";
    ConstMembers[ConstMembers["fixedNumNum"] = 3] = "fixedNumNum";
})(ConstMembers || (ConstMembers = {}));

computed members?

let num:Array = ['进击的切图仔',18,'男']
enum ComputedMenbers{
    sex = num.pop(),
    age = num.pop(),
    nameLen = num[0].length,
    name = num.shift(),
    random  = Math.random() | 0
}

编译后的 js:

var num = ['进击的切图仔', 18, '男'];
var ComputedMenbers;
(function (ComputedMenbers) {
    ComputedMenbers[ComputedMenbers["sex"] = num.pop()] = "sex";
    ComputedMenbers[ComputedMenbers["age"] = num.pop()] = "age";
    ComputedMenbers[ComputedMenbers["nameLen"] = num[0].length] = "nameLen";
    ComputedMenbers[ComputedMenbers["name"] = num.shift()] = "name";
    ComputedMenbers[ComputedMenbers["random"] = Math.random() | 0] = "random";
})(ComputedMenbers || (ComputedMenbers = {}));

在这里可以明显的看得出常量成员计算成员 的差异, 前者可以在编译阶段求值,而后者则是在编译后执行时求值的.

常量枚举
大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义.

const enum ConstEnum{
    A,B,C
}

let Arr:Array = [ConstEnum.A,ConstEnum.B,ConstEnum.C]

编译后的 js:

var Arr = [0 /* A */, 1 /* B */, 2 /* C */];

常量枚举不同于普通的枚举,并且不能包含计算成员, 他会在编译阶段就被清除掉,
编译后的代码没有关于枚举的数据, 这是为了减小开销和额外的非直接的对枚举成员的访问, 只有用到它的时候才会被内联进来,大大提升性能;

interface 接口

接口可以用来约束对象,函数以及类的结构和类型.

对象类型接口

假设我们在请求用户列表接口时,为了更好约束接口的数据结构和类型,我们可以这样定义

//定义成员接口
interface Member{
    readonly id: string; // id 为字符串 且只读,不可修改
    nick_name: string; //昵称为字符串
    age: number;
    [other: string]: any;//表示可以为其他字段, 类型随意
}

//定义列表
interface UserList{
    data: Array;//这里也可以将类型携程 Member[]
    code: number;
    msg?: string; // ? 表示可有可无
}

let list:UserList = {
    data: [
        { id: '1', nick_name: 'FPX', age: 18, mvp: 'xt' },
        { id: '2', nick_name: 'IG', age: 18, win: true },
        { id: '3', nick_name: 'RNG', age: 18 }
    ],
    code: 20000
}

let renderList = (ul: UserList):void => {
    ul.data.forEach((item) => {
        console.log(item)
    })
}

renderList(list);

//编译后输出: 
//      { id: '1', nick_name: 'FPX', age: 18, mvp: 'xt' },
//      { id: '2', nick_name: 'IG', age: 18, win: true },
//      { id: '3', nick_name: 'RNG', age: 18 }

这里可能会有同学有个疑问, 如果我把 Members 接口中的 [other: string]: string 去掉,接口数据是不是就不能含有其他字段了,这边拥有定义的nick_name,idwin字段了?

答案是: 不是这样,接口依然可以拥有别的字段,只不过定义的那几个字段一定要包含, ts 考虑过这种情况, 其实就和 鸭子模型 一样, 只有你会嘎嘎嘎,游泳就行,鸭子的特征你拥有我就认为你是一只鸭子;

但是在写对象字面量时编译器依然会发起警告,这个该如何解决呢? 这里有三种解决方法:

// 第一种
renderList({
    data: [
        { id: '1', nick_name: 'FPX', age: 18, fmvp: 'xt' },
        { id: '2', nick_name: 'IG', age: 18, win: true },
        { id: '3', nick_name: 'RNG', age: 18 }
    ],
    code: 20000
} as UserList); //告知编译器你自己明确他就是 UserList 接口要的类型,绕过类型检测

// 第二种
renderList({
    data: [
        { id: '1', nick_name: 'FPX', age: 18, mvp: 'xt' },
        { id: '2', nick_name: 'IG', age: 18, win: true },
        { id: '3', nick_name: 'RNG', age: 18 }
    ],
    code: 20000
}); // 这种写法和第一种是等价的,不过在 React 中不推荐,会被当成 jsx 语法产生歧义;

//第三种就是使用 字符串索引签名
//也就是在 Member 接口中添加 `[other: string]: string` 类型检测

函数类型接口

此前我们学到过关于函数的类型检测.

  • 第一种
var fun = (x:number,y:number):number => x + y;

fun(1,1); // 2
  • 第二种
var fun: (x:number,y:number) => number

fun = (a,b) => a + b;
  • 第三种

另外还可通过类型别名定义, 给一个函数类型起一个名字

type Fun = (x:number,y:number) => number

var fun:Fun = (a,b) => a + b
  • 第四种

通过简单的接口定义函数类型

interface Fun{
    (x:number,y:number): number
}

var fun:Fun = (a,b) => a + b

通过接口定义函数的方法大致了解清楚了, 接下来我们来梳理一些关于函数的其他知识点; 在上述四种方法中,后面三种只是定义了函数的类型,并未实现函数, 第一种定义方法在定义了函数的类型同时有实现了函数;

在此我们再来梳理一下函数知识点的梳理

函数参数不可多页不可少

let sum = (a:number,b:number):number => a + b;

sum(1,2) // ok

//只传一个参数
sum(1) // Expected 2 arguments, but got 1. => 预期有2个参数,但有1个。

//多传一个参数
sum(1,2,3) // Expected 2 arguments, but got 3. => 预期有2个参数,但有3个。

ts 中的函数相对于 js 的函数来说,对参数的个数更为严谨,更能培养我们的代码质量.

可有可无的参数可以设置为 可选参数

注意一点, 可选参数必须位于必选参数之后

let sum = (a:number,?b:number):number => a + b||0;

sum(1,2) // 3
sum(1) // 1

函数参数默认值

let sum = (a:number,b = 22, ?c:number ):number => a + b + c || c;

sum(1); // 23
sum(1,2); // 3
sum(1,2,3); // 6

不确定参数时可使用 剩余参数

let sum = (...res: number[]): number => res.reduce((a,b) => a + b);

sum(1,2); // 3
sum(1,2,3); // 6
sum(1,2,3,4); // 10

函数重载

函数重载在 Java 中极为常见, 函数重载就是几个函数名相同但是参数个数/类型不同的函数, 这样的好处是在功能相似的情况下无需选用其他函数,增强了函数的可读性;

例如实现一个函数, 如果是 数字 的话就进行累加, 如果是 字符串 的话则进行大写拼接

function merge(...num:number[] ):number
function merge(...str: string[]): string
function merge(...arg: any): any{
    if (typeof arg[0] === 'number') {
        return arg.reduce((a,b) => a + b)
    } else if (typeof arg[0] === 'string') {
        return arg.reduce((a,b) => a + b).toUpperCase()
    }
}

merge(2, 343, 54), // 399 
merge('da','dsa','wed') // 'DADSAWED'

利用混合类型的方法定义一个简单的计算类库, 可以让一个函数使用起来就和对象一样,拥有自己的属性和方法;

type calcFun = (...nums: number[]) => number

interface calculator{
    ():void,
    add: calcFun,
    multiply: calcFun,
    version: string
}

var calc: calculator = (() => {}) as calculator;// 跳过类型检测
calc.add = (...num) => num.reduce((pre, cur) => pre + cur);
calc.multiply = (...num) => num.reduce((pre) => pre * cur);
calc.version = '0.0.1'

calc() //undefined
calc.add(1,2,3) // 6
calc.multiply(1,2,3,4) // 24
console.log(calc.version) // '0.0.1'

ts 中的类覆盖了 ES6 中的类,同时增加了其他特性.

public 公有属性(默认)

class Person{
    constructor(name,age,sex){
        this.name = name;
        this.age = age;
        
        // 一旦设置了类的属性,就必须给其初始值
        
    }
    sex: string
    age?: number //可选属性
}

private 私有成员

private 私有成员只能被类本身调用,而不能通过实例或者子类继承且调用, 另外,如果将 constructor 也私有化的话, 那么这个类则不能被实例化和继承!

class Person{
    /* private */  constructor(name,age,sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    sex: string = '18'
    age: number //可选属性
    private say(){
        console.log('嘤嘤嘤')
    }
}

// 如果将 constructor 私有化的话 , Person 则无法被实例化, 会报错
let person = new Person('资深切图大师',undefined,'秘密');
    
    person.say() // 无法通过实例化调用, 报错

// 如果将 constructor 私有化的话 , 则无法被 Son 继承
class Son extends Person{
    constructor(name,age,sex,myProp){
        super(name,age,sex)
        this.sonProp = myProp
        
        this.say() // 无法通过子类实例调用, 二连错
    }
}

protected 受保护成员

一个受保护成员,只能在类或者子类中使用,而不能通过类实例化使用, 如果将 constructor 设置为受保护成员, 那么这个类则不能被实例化, 但是可以被继承,成为一个鸡肋 = > (基类)

class Person{
    /* protected */  constructor(name,age,sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    sex: string = '18'
    age: number //可选属性
    protected say(){
        console.log(' protected ')
    }
}

// 如果将 constructor 设置为 `protected` 的话 , Person 则无法被实例化, 会报错
let person = new Person('资深切图大师',undefined,'秘密')
    person.say(); // 报错, 该方法不能通过实例对象使用

class Son extends Person{
    constructor(name,age,sex,myProp){
        super(name,age,sex)
        this.sonProp = myProp
        this.say() //允许被子类实例使用
    }
}

readonly 只读属性

设置为 readonly 后则不能再次修改

class Person{
    constructor(name,age,sex){
        this.name = name;
        this.age = age;
        
        // 一旦设置了类的属性,就必须给其初始值
        
    }
    sex: string
    readonly age: number = 18 //可选属性
}

static 静态成员

利用 static 设置的 属性, 只能通过构造函数调用,不能通过实例对象调用,当然,类的静态成员同样也可以被继承

class Person{
    //如果给构造函数的参数也设置状态的话, 它会自动挂载到类上面去
    constructor(public name:string = '进击的切图仔',sex:string){
        this.sex = sex;
    }
    sex:string
    static money:number = -23333
}
let person = new Person('切题仔','男');
console.log(Person.money) // -23333 爸爸的花呗

class Son extends Person{
    constructor(name, sex, money) {
        super(name,sex)
        this.name = name;
        this.money = Son.money + money
    }
    name: string
    sex: string = '男'
    money:number
}

let son = new Son('切图仔仔','男',333)
console.log(son.money) // -23000 儿子继承了爸爸的花呗后单纯流出了敢动的泪水

抽象类

何为抽象类: 只能被继承而不能被实例化的类;

在 js 的类中,并没有向 php 等其他语言一样拥有抽象类的概念, 在 ts 中增加了这一特性

抽象类的好处就是可以抽离出事物的共性,有利于代码的复用和拓展

abstract class Phone{
    constructor(){
        
    }
    //在抽象类中, 也可以不指定方法的具体实现,待到子类在有子类实现即可 例如手机开机的画面都不一样
    
    //抽象方法的好处就是 每个子类都有明确的实现方法, 那么父类就没必要实现
    abstract boot() : void //开机
    
    call(phoneNumber){//拨打电话
        console.log(`正在拨打 ${phoneNumber} ...`)
    }
}

class Mi extends Phone{
    constructor(){
        super()
    }
    
    boot(){
        console.log("Mi 的开机画面, Are you ok! ")
    }
}


class OnePlus extends Phone{
    constructor(){
        super()
    }
    
    boot(){
        console.log("不将就")
    }
}

// let phone = new Phone() // bad 无法实例化
let mi = new Mi();

    mi.call('1008611') // '正在拨打 1008611 ...'
    
let one_plus = new OnePlus();
    
    one_plus.boot() // "不将就"

另外, 抽象类还有一个好处, 就是可以用来实现多态

多态

何为多态: 同一接口由于不同的调用者进行调用,调用结果也会大不相同

// 上述代码急速嵌入 coding . . .

let myPones:Phone[] = [mi,one_plus];

// 全部开机开黑挂机刷人头
myPones.forEach( phone => phone.boot() ) // 实例不同, 方法也不同,但是拥有一个统一的接口, 这样就实现了多态

同时,多态还有另外的一种使用方法,和 this 有关, 相信小伙伴们在学习 js 中的 this 指向的时候知道随着调用者的不同,this 的指向也不同;

class Person{
    protected constructor(name:string){
        this.name = name;
    }
    name: string
    static money: number = -999  // 父类的资产
    
    shopping(money:number){  //买东西
        Person.money -= money;
        return this;
    }
}

class Son extends Person{
    constructor(name){
        super(name)
    }
    
    getLuckyMoney(money){  //收红包
        Person.money += money
        return this;
    }
}

let son = new Son('切图仔仔') // 儿子出生了

son
    .getLuckyMoney(1000)
    .getLuckyMoney(1000)
    .getLuckyMoney(1000)//得到了家人大爱戴, 收红包收到手软
    .shopping(233) // 买了最喜欢的零食
    .getLuckyMoney(1000)
    
    console.log(Person.money) // 2768

随着 this 的不同指向, 子类既可以使用父类的方法,同时也可以使用自身的方法, this 无缝切换, 这样保证了子类和父类之间接口的连贯性.

类与接口的关系

  • interface : 只声明成员方法而不做具体实现;
  • class : 声明且实现方法;
  • 类可以实现(implements)接口;
  • 类可以继承于类; (上面以做过例子,下面不在做演示 ⏫)
  • 接口可以继承于(多个)接口;
  • 接口可以继承与类;
  • 当接口继承于类时,会抽离出类的成员方法(pulbic,private,protect);

类可以实现(implements)接口

类在实现接口的过程中, 可以定义属于自己的成员属性, 同时接口只能约束类的 公有成员构造函数

interface PersonInterface{
    name: string, // 名字
    age: number,  // 年龄
    sex: string,  // 性别
    say(con:string):void,
    birthday?: string, // 生日 可选
    like?: string[] // 爱好 可选
}


// 利用 Person 实现 PersonInterface 接口


class Person implements PersonInterface{
    constructor(name,age,sex,birthday?,like?) {
        this.name = name
        this.age = age
        this.sex = sex
        birthday && (this.birthday = birthday)
        like && (this.birthday = like)
    }
    
    mantra: '嘤嘤嘤'
    
    name: string // 名字
    age: number  // 年龄
    sex: string  // 性别
    birthday: string = '秘密' // 生日 可选
    like: string[] = ['篮球', '唱歌', 'Coding', 'CV大法'] // 爱好 可选

    say() {
        console.log(this.like)
    }
}

let person = new Person('切图仔', 18, '男')

person.say() // ['篮球', '唱歌', 'Coding', 'CV大法']

一个接口可继承于(一个或多个)接口

interface I1{
    a: string
}

interface I2{
    b: boolean
}

interface I3{
    c: number
}

// I 接口同时继承了  I1, I2, I3 接口
interface I extends I1, I2, I3{ }

// 等价于
interface I{
    a: string,
    b: boolean,
    c: number
}

接口可以继承与类

接口不仅可以继承接口, 还可以继承类, 相当于接口吧类的成员方法都抽象了出来(也就是只有类的成员结构,而没有具体的实现出来)

接口在抽离类的时候,不仅抽离了类的公共成员,而且还抽离了私有成员和受保护成员

class Browser{
    openBiliBili(): void { }
    supportJS: true
    supportCSS3: boolean = true
    supportHTML5: boolean = true

    private supportES6: true
}

// 定义浏览器接口

interface BrowserInterface extends Browser{ }

//在这里不用特别的声明 Chrome 的成员方法,因为他已经完美地继承了 Browser 的成员方法
class Chrome extends Browser implements BrowserInterface{ }

class IE implements BrowserInterface{
    openBiliBili(): void { }
    supportJS: true
    supportCSS3: false
    supportHTML5: false
    supportES6: false
}

//Class 'IE' incorrectly implements interface 'BrowserInterface'.   Property 'supportES6' is private in type 'BrowserInterface' but not in type 'IE'.

// 类“ IE”错误地实现了接口“ BrowserInterface”。属性“ supportES6”在类型“ BrowserInterface”中是私有的,但在类型“ IE”中不是私有的。 

//这是因为 IE 并不是 Browser 的子类, 自然而然也就不能包含 Browser 的非公有成员
// 所以 IE 不是浏览器 (滑稽)

可以看出接口的基础,既可以抽离出可重用的接口, 也可以将一个接口合并成一个接口

泛型

什么是泛型?

泛型就是不预先确定数据类型,集体的类型在使用时才知道;

那么泛型有啥好处呢? 如果不确定的话,我们直接写 any 类型不就完事了?

其实使用泛型是有好处的, 如果使用 any 的话,就等于失去了 ts 的灵魂, 就像给聋子听歌一样, 泛型的好处还是有的,下面举个例子

//定义一个上传图片的函数
let uploadImage = function(imageData:string):void{
    // 此处省略 n 行代码
}

//没有毛病

// 可以正常调用
uploadImage('假装是图片资源')

就在你已经实现了功能函数的时候, 产品锦鲤用六亲不认的走姿向你走来, 并和你说了一句, 兄dei, 图片上传更改为可多张上传

这个时候该怎么办呢? 嗯哼,突然想起来可以用上面的函数重载来解决这个问题

function uploadImage(imageData: string):void
function uploadImage(imageData: number[]):void
function uploadImage(imageData: any) {
    if (typeof imageData === 'string') {
        //单张图片上传
    } else if (Array.isArray(imageData)) {
        //多张图片上传
    }
}

但是这样会觉得有点多, 可以在简单些, 这个时候就可以用到我们的泛型了

//利用泛型改造函数的几种方式
// 定义函数
function uploadImage(imageData:T): void{
     if (typeof imageData === 'string') {
        //单张图片上传
    } else if (Array.isArray(imageData)) {
        //多张图片上传
    }
}

// 函数别名定义一个函数类型
type uploadImage1 = (imageData: T) => void

let fun1:uploadImage1 = uploadImage

// 利用接口定义一个函数
interface uploadImage2{
    (imageData:T): void
}

/*
    如果不要接口的其他成员也受到泛型的影响的话,可以这样使用
    
    interface uploadImage{
        (imageData:T): void
    }
    
    这样的话必须指定明确的类型, 如果不明确类型的话,也可以给 T 一个默认类型 如 uploadImage  
    
    let func: uploadImage = uploadImage
*/

let fun2: uploadImage2 = uploadImage

// 调用函数
fun2('图片1')
fun2(['图片1', '图片2'])

//也可以用 ts 的类型推断自动判断类型即可
fun2('图片1')
fun2(['图片1', '图片2'])

泛型类与泛型约束

//利用泛型定义一个类
// 利用泛型定义一个类需要注意一个点,就是静态成员不能引用类类型参数,否则会报错

class Person{
    constructor(name:string){
        this.name = name
    }
    name: string
    /* static */hiking(equipment:T){
        console.log(`${this.name}的装备:`,equipment)
    }
}

let p1 = new Person('p1');
    p1.hiking(['口罩', '口罩', '口罩']) //p1的装备: ['口罩', '口罩', '口罩']
    
let p2 = new Person('p1'); // 在泛型类中, 如果不明确指定类型的话就是 anyany 类型
    p2.hiking('') //p1的装备: ''

还有我们可以对泛型进行一些约束条件

interface PersonInterface{
    readonly name: string,
    temperature: number,
    mask: boolean
}

// 因为 T 没有 name, temperature, mask,所以可以利用接口继承给予约束, 参数的格式也必须符合接口,否则则报错 

function checkpoint(person: T): void{
    if (person.temperature >= 37.2) {
        console.log(`隔离${person.name}`)
    } else if (person.mask) {
        console.log(`给与${person.name}口罩`)
    }
}


checkpoint({name: 'p1',temperature: 38,mask: true})
checkpoint({name: 'p2',temperature: 36,mask: true})
checkpoint({name: 'p3',temperature: 36.7,mask: false})

//result => '隔离p1' '给与p2口罩'

以上的例子得出了泛型的好处有:

  • 函数和类可以轻松支持多种数据类型,增强程序的拓展性
  • 不必写多条的函数重载, 多种数据类型联合声明, 增强了代码的可读性
  • 灵活控制类型间的约束

这篇文章是笔者在学习 typescript 后利用工作之余的时间为小伙伴一字一码结合自己的实践经验写的学习总结, 为大家分享一下 typescript 的魅力, 今后笔者还会继续分享更多有趣的干货和文章, 赶紧点击关注, 在看和分享吧!

进击的切图仔

你可能感兴趣的:(万字长文助你打好 TS 基础)