TypeScript 学习笔记

320 阅读5分钟

前言

作为一个职场菜鸟,到了公司发现大家已经早就没有用原始的JS了,所有前端代码都是用TypeScript 写的,于是匆匆忙忙学习了TS,这里分享自己的学习笔记,希望能帮助一些和我一样的初学者,么么哒!!!

1、开发环境搭建

(1)采用brew工具下载安装node 

注意:最好不要直接官网下载node进行安装,会导致很多卸载残留 brew安装方法: 命令行输入:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

(2)安装node和yarn 

这里使用yarn作为工具,作用和npm类似 ,你也可以使用npm

使用brew进行node安装,命令行输入命令:

brew install node

(3)安装TypeScript

 命令行输入:

yarn global add typescript

2、编译ts文件 

进入文件目录,执行命令:tsc ***.ts,此时该文件夹下生成一个同名的js文件,这个js文件就可以直接在浏览器运行。(自己练习的话可以使用在线编译工具

3、TS的类型 

(1)指定变量类型 

声明一个变量并指定它的类型语法: let a : type(type指的就是变量类型)

当一个变量声明了类型之后,就只能给变量赋该类型的值(any和unknown类型比价特殊,后面会介绍),如下:

 let a: number; 
 a = 10; 
 a = 'str' // 当给a赋值一个字符串时会报错 

 如果变量声明和赋值是同时进行的(如:let a = 123),则TS可以根据所赋的值自动检测类型,以后修改变量值时也只能修改成一样类型的值。

 (2)TS数据类型总结 

  • number: 任意数字 

    let a: number = 1

  • string: 任意字符串

    let str: string = 'hello'

  • boolean: 布尔值 

    let isInteresting: boolean = true

  • any:任意类型

    let value: any = 'showee'
    value = 100  //不报错
    let num: boolean = value  // 不报错
    let obj: any = {}
    obj.getName() // 不报错 
    

any类型的变量可以获取它的任何属性和方法,当一个变量的类型设置为any后,相当于关闭了该变量的TS的类型检测,因此不建议使用any类型。

如果声明一个变量时不指定类型,则TS解析器会自动判断变量的类型为any,如下所示:

let d;  // 不指定类型,只声明了变量
d = 10;  // 不报错
d = 'hello';  // 不报错
d = true;   // 不报错
  • unknown:类型安全的any

表示某个变量的类型是未知的,也意味着这个变量可能是任意类型的值,这个值可能是来自于动态内容。

let value: unknown = 'showee'
value = 100  //不报错
let num: boolean = value // 报错!这里和any的表现不同
let obj: unknown = {
    getName: function() {}
}
obj.getName() // 报错!

我们可以通过类型守卫(如:typeof)来收窄变量的类型范围

let value: unknown = 100
let num: number
if (typeof value === 'number') {
    num = value  // 此时不报错
}

或者通过类型断言来告诉解析器变量的实际类型

let e: unkown;
let s: string;

s = e as string;  // 写法一:变量 as 类型
s = <string>e;   // 写法二:<类型>变量 
  • void:没有值(或undefined),常用来设置没有返回值的函数

    function fn(): void{ return; }

  • never:不能是任何值,常用来设置函数不会返回结果 

    function error(message: string): never { throw new Error(message); // 抛出错误,函数并没有返回值 }

    function infiniteLoop(): never { while (true) {} // 函数没有返回值 }

  •  - object:任意的JS对象

    let obj: object = { name: 'showee' }

    // 用{}来制定对象中可以包含哪些属性 let obj1: {name: string, age?: number}; // 可以用?来表示可选属性 obj1 = {name: 'showee'}

    let obj2: {name: string, [propName: string]:any}; // [propName: string]:any 表示除了name之外还可以添加任意类型的属性

  • Object:看起来object和Object只有大小写的区别,Object实际上表示的范围更加的广泛,除了表示对象还可以表示数字、字符串、布尔值等基本类型(但不包括Null和Undefined)

    const obj: Object = { name: 'showee' } const obj: Object = 'showee' const obj: Object = 100 const obj: Object = true

    const obj: {} = { name: 'akara' } // 这种写法{},这和Object是完全等价的

通常表示对象建议使用object而不是Object

  • array:任意js数组
// 两种方式
let arr: number[] = [1, 2, 3]
let arr: Array<number> = [1, 2, 3]
  • tuple:元组,TS新增类型,固定长度的数组

    let person: [string, number] = ['showee', 3]

  • enum:枚举,TS中新增类型

    enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

    console.log(Days["Sun"] === 0); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true

    console.log(Days[0] === "Sun"); // true console.log(Days[1] === "Mon"); // true console.log(Days[2] === "Tue"); // true console.log(Days[6] === "Sat"); // true

  • 字面量类型:限制变量的值就是该字面量的值

    // 字符串字面量类型 let str: 'small' = 'large'

    // 数字字面量类型 let num: 1 = 1

    // 布尔字面量类型 let boo: true = true

注意,用let和const声明变量时,其类型是不同的,如下:

let name = 'aka' // string
const name = 'aka' // 'aka' 

4、函数

(1)参数

可选和默认参数:TS中函数对参数的长度要求很严格,可使用?表示可选参数,也可以使用参数默认值:

function A(name: string,age?:number): string | undefined {
    return name;
}

function B(name: string = 'showee'): string {
    return name;
}

剩余参数:当需要同时操作多个参数,或者你并不知道会有多少参数传入,此时可以把所有参数收集到一个变量里面

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("xiao", "ke", "ai");

(2)匿名函数

匿名函数中会自动推导参数的类型

let arr = [1, 2, 3]
arr.forEach(item => console.log(item))  // 不需要给item添加类型注解

(3)this

在对象字面量中的this类型可能有多种:

 如果这个方法显示指定了this参数,那么this具有该参数的类型

let bar = {
  x: 'hello',
  f(this: { message: string }) {
    this; // { message: string }
  }
};

否则,如果方法由带this参数的签名进行上下文键入,那么this具有该参数的类型

let foo = {
  x: 'hello',
  f(n: number) {
    this; // { x: string, f(n: number): void }
  }
};

否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中包含由 ThisType 键入的上下文类型,那么 this 的类型为 T 

否则,如果--noImplicitThis 选项已经启用,并且对象字面量中不包含由 ThisType 键入的上下文类型,那么 this 的类型为该上下文类型 

否则,如果 --noImplicitThis 选项已经启用,this 具有该对象字面量的类型 

否则,this 的类型为 any

(4)重载

为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用,下面是来自官网的一个示例

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

5、类

(1)定义和使用

class Person{
    // 定义实例属性
    name: string;
    age: number;
    readonly id: string  = 'sfijper'
    
    // 在属性前使用static关键字可以定义类属性(静态属性),直接通过类访问
    static gender: string = 'female';
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age
    }
    
    // 实例方法,如果在方法名前面加static则成为静态方法,静态方法直接通过类调用
    sayHi() {
        return `Hi!I am ${this.name},I am ${this.age} years old`
    }
}

const xiaohong = new Person('xiahong', 3);
console.log(Person.gender)  // 静态属性的访问方法
xiaohong.id = 'sofj'  // 报错,只读属性不能修改
console.log(xiaohong.sayHi())  // 输出 "Hi!I am xiahong,I am 3 years old" 

(2)继承

使用继承后,子类会拥有父类所有的方法和属性,通过继承的方式可以将多个类中共有的代码写在一个父类中,这样只需要写一次即可让所有子类同时拥有父类中的属性和方法,并且可以在子类中添加一些分类中没有的属性和方法。在子类中添加和父类相同的方法,可以覆盖掉父类中的该方法,这种称为方法重写。

class Person{
    // 定义实例属性
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age
    }
    
    sayHi() {
        console.log(`Hi!I am ${this.name},I am ${this.age} years old`) 
    }
}

// 使Boy继承Person类
class Boy extends Person{
    sing() {
        console.log('lalalalala...')
    }

    // 添加和父类相同的方法,可以覆盖掉父类的
    sayHi() {
        console.log('I am a cool boy')
    }
}

const xiaohong = new Person('xiaohong', 3);
xiaohong.sayHi();   // "Hi!I am xiaohong,I am 3 years old" 
const boy = new Boy('xiaoshuai', 3);
boy.sing();  // "lalalalala..." 
boy.sayHi();  //  "I am a cool boy"

super关键字:在类的方法中,super就表示当前子类的父类,如果需要在子类中写构造函数,则在子类的构造函数中必须使用super()调用父类的构造函数

class Person{
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age
    }
    
    sayHi() {
        console.log(`Hi!I am ${this.name},I am ${this.age} years old`) 
    }
}


class Boy extends Person{
    id: string
    constructor(name: string, age: number, id: string) {
        super(name, age);
        this.id = id;
    }
    sayHi() {
        super.sayHi();
        console.log(`Hi!I am ${this.name},I am ${this.age} years old,my id is ${this.id}`) 
    }
}

const xiaohong = new Person('xiaohong', 3);
const boy = new Boy('xiaoshuai', 3, 'abcdefg');
xiaohong.sayHi();
boy.sayHi();

(3)抽象类

以abstract开头的类是抽象类,抽象类和其他类区别不大,只是不能用来创建对象,但是可以被继承(通常我们创建的抽象类就是专门用来被继承的,用来作为父类)。

抽象类中可以添加抽象方法,抽象方法使用abstract开头,没有方法体。抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写。

abstract class Person{
    name: string;

    constructor(name: string) {
        this.name = name;
    }
    
    abstract sayHi(): void;
}

class Boy extends Person{
    // 必须重写抽象方法
    sayHi() {
        console.log(`I am a boy`) 
    }
}
const person = new Person();  // 报错 Cannot create an instance of an abstract class.
const boy = new Boy('xiaoshuai');
boy.sayHi();

(4)接口

接口用来定义一个类的结构,它定义了一个类中应该包含哪些属性和方法,同时接口也可以当成类型声明去使用。

interface myInterface{
    name: string;
    age: number;
}
// 当定义两个同名的接口是=时,相当于合并在一起的效果
interface myInterface{
    gender: string;
}
const obj: myInterface = {
    name: 'showee',
    age: 3,
    gender: 'male'
}

接口中的所有属性都不能有实际值,并且所有方法都是抽象方法。

定义类时,可以使类去实现一个接口(实现接口就是指使我们定义的类满足接口的要求)。

interface myInterface{
    name: string;
    age: number;

    say(): void;
}

class myClass implements myInterface{
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    
    say() {
        console.log('hello')
    }
}

(5)属性的封装

TypeScript 有三种访问修饰符, public、private 和 protected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的

    class Animal { public name: string; public constructor (name) { this.name = name; } }

  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问和修改,我们可以在类中添加方法使其能够被外部访问和修改(即getter和setter,称为属性存取器)

    class Person{ private _name: string;

    constructor(name: string) {
        this._name = name;
    }
    
    //  定义方法,获取name属性
    get name() {
        return this._name
    }
    
    // 定义方法,修改name属性
    set name(value: string) {
        this._name = value
    }
    

    }

    const person = new Person('showee'); console.log(person.name); // 'showee' person.name = 'erbao'; console.log(person.name); // 'erbao'

  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

    class Person{ protected name: string;

    constructor(name: string) {
        this.name = name;
    }
    

    }

    class Boy extends Person{ test() { console.log(this.name) } }

    const xiaohong = new Person('xiaohong'); const boy = new Boy('xiaoshuai'); console.log(xiaohong.name) // 报错 Property 'name' is protected and only accessible within class 'Person' and its subclasses. boy.test(); // ‘xiaoshuai’

6、泛型

在定义函数或类时,如果遇到类型不明确的就可以使用泛型,可以同时指定多个

function fn<T>(a: T): T{
    return a;
}
// 可以直接调用具有泛型的函数
let res1 = fn(10);  // 不指定泛型,TS可以自动度类型进行推断
let res2 = fn<string>('hello');  // 指定泛型
console.log(typeof res1, typeof res2);


// 接口中使用泛型
interface Inter{
    length: number;
}
// T extends Inter 表示泛型T必须是Inter实现类(子类)
function fn1<T extends Inter>(a: T): number{
    return a.length;
}


// 类中使用泛型
class MyClass{
    name: T;
    constructor(name: T) {
        this.name = name;
    }
}
const myClass = new MyClass<string>('showee');

7、类型操作

(1)联合类型

联合类型表示取值可以为多种类型中的一种。

let value: string | number;
value = 'showee';
value = 123;

当不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

function getLength(something: string | number): number{
    return something.length;
}
// 这里会报错,因为length不是string和number的共有属性



function getString(something: string | number): string{
    return something.toString();
}
// 这里不会报错,因为toString是string和number的共有属性

(2)交叉类型

type a = {
    name: string;
}
type b = {
    age: number;
}
let obj: a & b = {
    name: 'showee',
    age: 0,
}

(3)类型断言

当你非常了解一个值的类型的时候,你可以通过断言的方式告诉编译器你的想法,这有些类似于其他语言中的类型转换,类型断言有以下两种方式

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; // 尖括号语法


let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;  // as语法

(4)类型保护(类型守卫)

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。

类型保护与特性检测有些类似,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护

  • typeof

在JS中typeof可以用来获取变量的基本类型,而在TS中,可以获取类型

function doSome(x: number | string) {
  if (typeof x === 'string') {
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }

  x.substr(1); // Error: 无法保证 `x``string` 类型
}
  • in关键字

    interface Admin { name: string; privileges: string[]; }

    interface Employee { name: string; startDate: Date; }

    type UnknownEmployee = Employee | Admin;

    function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges" in emp) { console.log("Privileges: " + emp.privileges); } if ("startDate" in emp) { console.log("Start Date: " + emp.startDate); } }

  • Instanceof

    class Foo { foo = 123; common = '123'; }

    class Bar { bar = 123; common = '123'; }

    function doStuff(arg: Foo | Bar) { if (arg instanceof Foo) { console.log(arg.foo); // ok console.log(arg.bar); // Error } if (arg instanceof Bar) { console.log(arg.foo); // Error console.log(arg.bar); // ok } }

    doStuff(new Foo()); doStuff(new Bar());

  • 自定义类型保护的类型谓词

    function isNumber(x: any): x is number { return typeof x === "number"; }

    function isString(x: any): x is string { return typeof x === "string"; }

(5)类型别名

类型别名用来给一个类型取一个新的名字

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

(6)keyof

该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

interface Person {
  name: string;
  age: number;
  location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string