这是最近复习TypeScript的一份笔记

为什么要学习TypeScript

TypeScript(以下简称ts)是微软开源的编程语言

在目前,ts还只是JavaScript的增强,最终还是要编译为JavaScript来运行,JavaScript太过于自由,导致我们经常犯一些简单的错误,而ts作为js的超集,是一个强类型语言,拥有面向对象编程语言的所有特性

ts可以说是未来前端开发必备的技术点,博主最近接触到的很多新项目都在使用ts进行构建,包括各大框架都采用ts来构建

准备开发环境

首先需要安装 node 开发环境,这个不必多说

然后使用npm i typescript -g来全局安装ts的工具,然后可以使用命令tsc -V来检查安装状态

安装

我们新建一个demo文件夹,新建一个first.ts文件

function say(msg: string): void {
    console.log(msg);
}
say("hello ts");

这里我们可以发现,方法的定义和常规的JavaScript有些区别,在参数的定义位置增加了一个:string,这个的作用就是类型限制,规定我们传入的参数必须为string类型,当然还有其他,我们下面会详细说

参数括号后面紧跟: void,这个就是规定方法的返回值类型,此时void为不返回,这个下面也会详细说

学过Java这类语言的朋友肯定会感到非常熟悉

我们可以在文件位置的命令行里面执行 tsc first.ts就可以编译文件了,执行完毕我们的目录里面增加了一个first.js文件,也就是说,我们的ts文件最终被编译为了js文件

如果你使用的vscode,可能编译完成以后你的代码会显示函数定义重复,此时你只需要在文件夹里面创建一个tsconfig.json 文件即可,当前不需要填充任何内容,也可以使用命令tsc --init生成配置文件

基础类型定义

在前面使用:string来规定了参数的数据类型,我们新建一个 typedemo.ts来测试一下基础类型的定义,我们可以使用tsc --watch typedemo.ts命令来监听文件修改并自动编译

为了方便测试,你可以安装nodemon,然后使用nodemon来运行编译后的js文件,这样就能实时看到结果了比如nodemon .\typedemo.js

//1 string 类型 
let stringdemo: string = "stringdemo";
//2 布尔类型
let booleandemo: boolean = false;
//3 数值
let numberdemo: number = 502;
//4 定义多个类型
let moretypedemo: number | string;
moretypedemo = 1;
moretypedemo = '1';
//5.1 数组定义方式1
let listdemo1: number[] = [1, 2, 7, 0, 7, 9, 9, 7, 0, 0];
//5.2 数组定义方式2
let listdemo2: Array<number> = [1, 2, 7, 0, 7, 9, 9, 7, 0, 0];
//5.3 数组定义方式3 指定个数和对应的类型
let listdemo3: [boolean, number] = [true, 1];
//6 any 类型 任意类型 基本无意义了
let anydemo: any = [1, true, 'hello']
//7.1 null 类型 null和undefined 是所有类型的子类型 比如 let str:string=null
let nulldemo: null = null;
//7.2 undefined 
let undefineddemo: undefined = undefined;
//8 Object 非原始类型 (string、boolean、number...这类为原始类型)
let objectdemo: object = undefined;
objectdemo = { name: 'ChouCong' };
//9 enum 枚举类型 补充常规的 常量定义方式 本质就是键值对
enum FONTSIZE {
    Large = 48,
    Middle = 36,
    Little = 24
}
//9.1 可以通过名字拿到值
let titleSize = FONTSIZE.Large;
console.log(titleSize);
//9.2 也可以通过值拿到名字
let fontType = FONTSIZE[48];
console.log(fontType);

/**
 * 10 never类型是任何类型的子类型
 * 但是没有类型是never的子类型
 * never一般指无限循环或者无法到终点
 * 可用来抛出错误
 */
function loop(): never {
    while (true) {
        console.log('我不会结束');
    }
}
function error(msg: string): never {
    throw new Error(msg);
}
/**
 * 11 as 类型断言,确定我们的数据类型, 当我们的数据有多个类型的时候
 * 有些操作需要确定我们的数据类型
 * 比如anyData为string|number类型,此时我们就不能直接使用anyData++
 * 我们可以使用 anyData as number,确定他就是一个数值型
 */
let anyData: string | number;
// anyData++;
(anyData as number)++;
对于never的解释

例子

接口

同样的我们新建一个 interfacedemo.ts来测试,按照上面的步骤将项目运行起来,在控制台查看运行结果

/**
 * 1.接口可以用来对你的数据制定规则 比如我们创建用户方法需要一个 User对象
 * 我们在接口中规定 User 对象里面每个属性的参数类型
 * 最后我们将 User 对象作为一个数据类型用来限定我们方法的传入参数
 */
interface User {
    name: string,
    age: number,
    //1.1参数后面跟 ? 代表这个参数不是必须的
    isAdmin?: boolean,
    //1.2跟 readonly 代表对象实例化成功以后 这个参数不能修改
    readonly uid?: number,
    //1.3对于其他不用明确规定的参数
    [otherName: string]: any,
}
// 添加学生
function create(user: User): string {
    //1.1 user.uid = 123; 因为 readonly 所以不能修改了
    return "添加成功";
}
const user: User = {
    name: 'ChouCong',
    age: 21,
    uid: new Date().getTime(),
    //1.3 [otherName: string]: any 作用体现
    sex: 1,
};
const result: string = create(user);
// console.log(result);

// 2.使用接口来约定函数方法
interface ResultUtil {
    (msg: string): string;
}
let resultImpl: ResultUtil = (msg: string): string => {
    //2.1 因为定义约束了返回值为 :string 所以不能 return false;
    return msg;
};
/**
 * 3. 使用接口来约束索引
 */
// 3.1 假如我们约束的索引为 数值 
interface UserListNumber {
    [index: number]: User;
}
const userListNumber: UserListNumber = [
    {
        name: 'ChouCong',
        age: 12
    }
]
console.log(userListNumber[0]);
// 3.2 假如我们约束的索引为 字符串
interface UserListString {
    [index: string]: User;
}
const userListString: UserListString = {
    user1: {
        name: 'ChouCong',
        age: 12
    }
}
console.log(userListString["user1"]);

// 4. 通过接口来约定类 也就是继承接口
interface Animal {
    //重量
    weight: number,
    // 中文名
    chName: string,
    // 动做
    say(msg: string): void
}
class Dog implements Animal {
    weight: number;
    chName: string;
    constructor(weight: number) {
        this.weight = weight;
        this.chName = "狗";
    }
    say(msg: string): void {
        console.log(`${this.chName}:${msg}`);
    }
}
let dog = new Dog(5000);
dog.say("wang wang");
// 5. 继承接口 新建一个保护动物接口
interface protectAnimal extends Animal {
    //保护等级
    protectionClass: number
}
// 实例化一个猫科保护动物类
class Felidae implements protectAnimal {
    protectionClass: number;
    weight: number;
    chName: string;
    say(msg: string): void {
        console.log(`${this.chName} - ${this.protectionClass}级保护动物:${msg}`);
    }
}
let panda = new Felidae();
panda.chName = "大熊猫";
panda.weight = 180000;
panda.protectionClass = 1;
panda.say("吃竹子")

/*
 * typescript 类的使用
 */

//1. 类的简单使用在前面的接口一节中已经简单说明
class Animal {
    chName: string;
    weight: number;
    say(msg: string): void {
        console.log(`${this.chName} :${msg}`);
    };
    //1.1 构造函数 当new一个对象的时候,可以直接传递初始值 比如 new Animal("dog", 5000);
    constructor(chName: string, weight: number) {
        // 通过this能够将实例化对象时传递的值赋值给类的属性
        this.chName = chName;
        this.weight = weight;
    }
}
//2 新增一个保护动物类继承动物类 并且新增一个保护等级属性
class protectAnimal extends Animal {
    protectionClass: number
    //构造函数
    constructor(chName: string, weight: number, protectionClass: number) {
        //2.1 super可以调用父类的构造函数 也就是将实例化protectAnimal的数据传递给Animal类的构造函数
        super(chName, weight);
        this.protectionClass = protectionClass;
    }
}

// let panda = new protectAnimal("大熊猫", 180000, 1);
// panda.say("吃竹子");

// 3. 类权限修饰
class User {
    // 3.1 private 私有的;就不能再通过 .name 拿到值了 只能类内部使用
    private name: string;
    /**
     * 4 get set 存取器
     * 通过private进行修饰的属性 实例化对象不能直接拿到
     * 可以通过 get set来操作
     */
    // 4.1 通过get来让实例化对象拿到 name 值
    get getName(): string {
        return this.name;
    }
    // 4.2 通过set来给 name 设置值
    set setName(name: string) {
        this.name = name;
    }
    //3.2 protected 子类能够拿到
    protected sex: number;
    // 3.3 static 静态属性 可以直接通过类拿到 User.avatar
    static avatar: string = 'https://avatar.v2ex.com';
    constructor(name: string, sex: number) {
        this.name = name;
        this.sex = sex;
    }
    say() {
        console.log(`user:${this.name}`);
    };
}
// 4. 测试 get set
// let user = new User("ChouCong", 1);
// console.log(user.getName);
// user.setName = 'lili';
// console.log(user.getName);
class stu extends User {
    constructor(name: string, sex: number) {
        super(name, sex);
    };
    say() {
        /**
         * console.log(`user:name=>${this.name},sex=>${this.sex}`); 
         * 3.1 处定义的private ,此处便不能使用 name属性
         * 3.2 定义的protected,此处可以拿到 sex 属性
         */
        console.log(`user:sex=>${this.sex}`);
    }
}

/**
 *  5. 抽象类 abstract
 * 抽象类不能被实例化,里面有常规的属性和抽象方法
 * 使用抽象类可以很好地指定一些规则来让继承的子类遵循
 * 抽象方法是规定子类必须实现的方法 
 * 抽象类和接口的区别:假如你的家族有任务,但是父辈完不成
 * 就会使用抽象方法来定义,让你来完成,加入你家族和你朋友的家族需要完成同样的任务
 * 就可以使用接口来定义这个共同任务,然后你们两个家族分别实现这个接口
 */
abstract class Article {
    abstract create(): void;
}
class TopArticle extends Article {
    create(): void {
        console.log("实现创建方法");
    }
}

//作为模块可以欺骗 ts,用来解决同文件夹不能定义同一对象名的问题 
export { };

函数

/*
 * typescript 函数相关,函数的简单使用在前面已经提过
 * 1. 新建一个计算圆面积的方法
 * 在函数里面我们同样需要约束参数的类型和返回值类型
 * @param r 半径
 */
function circleArea(r: number): number {
    return Math.PI * r * r;
}

// console.log(circleArea(5));

/**
 * 2. 你可以先声明方法 再具体实现
 */
var squareArea: (x: number) => number;
squareArea = function (x: number): number {
    return x * x;
}
/**
 * 3. 可选参数q
 * 新建一个计算长方形或者正方形面积的方法
 * 如果没有y传值,就代表是正方形
 * @param x 
 * @param y 
 */
function rectangleOrSquareArea(x: number, y?: number): number {
    return y ? x * y : x * x;
}
// console.log(rectangleOrSquareArea(2));


/**
 * 4. 方法重载
 * 方法重载就是多个同名的方法,但是可以不同的参数和不同的返回值
 * 这样做的好处就是我们可以通过一个具体方法去处理不同的情况
 * 比如这里我们定义一个 result方法
 * 第一方法定义 我们传递一个msg字符串 
 * 第二个方法定义 我们传递一个msg和一个code
 * 方法重载,参数越少的写在越上面
 * 第三个方法我们才来实现具体的方法,在方法内部去根据具体的传值情况来分别处理
 * 也就是当调用方法时没有传递code值,就默认设置为200
 */
interface Res {
    code: number,
    msg: string
}
function result(msg: string): Res;
function result(msg: string, code: number): Res;
function result(msg: string, code?: number): Res {
    if (!code) {
        code = 200;
    }
    return {
        code: code,
        msg: msg
    };
}
console.log(result('增加成功'));

//5. 默认值,当用户没有传递这个参数的时候给它一个默认值
function resultDefault(msg: string, code = 200): Res {
    return {
        code: code,
        msg: msg
    };
}
// 6. 剩余参数,当方法后面不确定有多少个参数,可以使用...来规定剩余参数
function stringsplice(mainstring: string, ...stringFra: string[]): string {
    stringFra.forEach(str => mainstring += str);
    return mainstring;
}
console.log(stringsplice("str1","str2"));

export { };

typescript中的this指向

/*
 * typescript 中的 this
 * 1. 举一个简单的案例来讲解一下 ts中的this
 */
// interface FooInter {
//     x: number;
//     y: number;
//     printX(): void;
//     printY(): void;
//     printZ(this: FooInter): void;
// }
class Foo {
    x = 3;
    y = 3;
    z = 3;
    printX() {
        console.log('x is ' + this.x);
    };
    printY = () => {
        console.log('y is ' + this.y);
    };
}

/**
 * 1.1 实例化一个Foo,并调用 printx 方法
 * 然后我们新建一个对象x,将Foo类的printX 方法赋值给 print属性
 * 然后调用x对象的 print方法,其实就是调用了 Foo类的printX方法
 * 输出:
 * x is 3
 * x is 10
 * 明明x.print()是调用的Foo.printX,而Foo里面的x值为10,这里却打印的是10
 * 因为此时Foo.printX方法里面的this指向已经变成 x对象了,而x对象的x值为10
 */
var foo = new Foo();
foo.printX();
var x = { x: 10, y: 10, print: foo.printX };
x.print();
/**
 * 1.2 解决这个问题我们可以使用 => 函数来锁定this指向为创建时指向
 * 我们新建一个printY方法来测试一下
 * 此时输出为:
 * y is 3
 * y is 3
 */
foo.printY();
var y = { x: 10, y: 10, print: foo.printY };
y.print();

泛型

/*
 * typescript 中泛型的使用
 * 1. 什么是泛型,使用过Java的很好理解
 * 下面举个例子(官方示例)我们有个方法identity,最开始我们给他传递数字
 * 后来要传递字符串,这时我们可以使用 any来定义参数类型
 * 我们可以使用泛型来替代any
 */
// function identity(arg: number): number {
//     return arg;
// }
// function identity(arg: any): any {
//     return arg;
// }

// 使用泛型替代 any 示例 方法后紧跟 <T> 这个T可以自定义名字,然后参数类型需要和这个 <...>中定义的名字相同
function identity<T>(arg: T): T {
    return arg;
}
// 此时我们就可以随意传递值了,当然传递的时候你可以说明你传递的泛型为字符串,也可以省略
let output = identity<string>('typescipt');

/**
 * 2. 你可能会问这个有啥用啊,再来举一个很常见的案例
 * 假如我们的数据库对象里面都有 增加和更新的方法,只是不确定传入的对象
 * 我们可以使用泛型封装一个DaoBase类,存储增加和更新功能
 */
class DaoBase<T>{
    create(createData: T): T {
        console.log(createData);
        return createData;
    };
    update(updateData: T) {
        return updateData;
    };
}
/**
 * 2.1 然后我们定义一个 User接口来规定用户类型
 * 再创建一个 UserDao 类继承 DaoBase基本类
 * 同时指定泛型为 User对象
 */
interface User {
    name: string;
    age: number;
}
class UserDao<User> extends DaoBase<User> {

}
let user: User = {
    name: 'ChouCong',
    age: 20
}
/**
 * 2.2 此时我们实例化UserDao 就可以传递User对象的值了
 */
let userdao = new UserDao();
let result = userdao.create(user);

/**
 * 3 泛型类型约束,我们可以对泛型做一些约束
 * 我们编写一个DataType接口,约束了一个number属性id
 * 我们新增一个create函数,传入泛型extends  DataType
 * 这其实就规定了这个函数传入的参数必须符合DataType的约束
 */
interface DataType {
    id: number
}
function create<T extends DataType>(arg: T): void {
    console.log(arg.id);
}
// 3.1 我们在调用函数的时候必须传入对象包含 id 属性
create({ id: 1 });

/**
 * 4 来一个官方约束案例
 * 我们新建一个getProperty函数,根据对象的键获取值
 * 我们使用泛型来制定传入的对象
 * 我们通过 K extends keyof T 来约束了传入的键必须是传入对象的键
 */

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
const book = { price: 12, number: 20 }
let value = getProperty(book, 'price')
// console.log(value);

export {
}

高级类型

/*
 * typescript中的高级类型
 * 1. 交叉类型(Intersection Types),交叉类型是将多个类型合并为一个类型,使用类型1&类型2表示
 * 来自官方的案例,我们创建一个extend函数
 * 传入两个对象,合并并返回,所以返回对象的类型就是传入的两个对象的交叉类型
 */
function extend<T, U>(first: T, second: U): T & U {
    // 存储返回的对象为交叉类型 T & U
    let result = <T & U>{};
    // 将传入的第一个对象循环存入 result
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    // 循环第二个对象,去重后存入result
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}
// 1.1 新建一个Person对象和ConsoleLogger类
class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
//1.2 传入extend,此时jim就有了Person对象和ConsoleLogger类全部属性
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

/**
 * 2. 联合类型(Union Types)
 * 取多个类型共同的部位
 * 比如我们定义 两个接口countType、stringType
 * 都具有hash属性和两个独立的函数
 * 此时我们定义data对象为  countType | stringType 联合类型
 * 我们的data对象就只能使用 hash ,而不能使用各自独立的函数
 */

interface CountType {
    hash: string;
    add_count();
}
interface StringType {
    hash: string;
    str_replace();
}
let data: CountType | StringType;
// data.hash
// data.add_count();

/**
 * 3. 类型保护
 * 如果使用了联合类型就不能使用特有的函数
 * 如果想要继续使用函数可以使用断言来确定对象的类型
 * 但是每次使用都需要断言判断
 */
// 3.1 断言的使用方式 注意虽然可以调用add_count,但是对象没有具体实例化,所以还是会报错
let newData: CountType | StringType ;
// if ((newData as CountType).add_count) {
//     (newData as CountType).add_count()
// }

/**
 * 3.2 我们可以新增一个函数,返回值就是data is CountType
 * 通过data is CountType ts就能判断这个data到底是什么类型
 * 然后在函数内部通过断言来判断 data是什么类型的对象
 */
// function isCount(data: CountType | StringType): data is CountType {
//     return (data as CountType).add_count !== undefined;
// }
// 3.2.1 使用定义的类型判断函数
// if (isCount(newData)) {
//     newData.add_count();
// }

/**
 * 3.3 来自官方的typeof类型保护示例
 * 通过 在函数isNumber和isString中使用typeof判断数据类型
 * 并返回x is number 和 x is string就能判断数据的类型
 */
function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}
// 3.3.1 具体使用
function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

export {

}