zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

Typescript学习笔记,从入门到精通,持续记录

2023-03-07 09:44:09 时间

资料来源

http://ts.xcatliu.com/

安装Typescript

1.安装

//安装
npm install -g typescript
//编译
tsc hello.ts
//初始化
tsc --init

TypeScript 最大的优势之一便是增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。

基础入门

Typescript可以理解为带静态类型的Javascript;

小贴士

Ts同样有着先声明后赋值、声明的同时赋值这些操作;

1.原始数据类型

  • 布尔值,boolean
  • 数值,number
  • 字符串,string
  • 空值,void 表示没有任何返回值的函数
  • Null 和 Undefined
/* 先声明后使用 */
let a: string;
a = "aaa";

/*声明的同时赋值*/
let b: string = "aaa";

/*类型推论*/
let c = false;
c = 1; //报错,类型推论得出c为布尔值

类型推论

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候有赋值,类型就会被推断为这个值得类型;

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

2.任意值(any)

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 类型,则允许被赋值为任意类型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7; 

3.联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

let myFavoriteNumber: string | number;
myFavoriteNumber = true;
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.

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

function getLength(something: string | number): number {
    return something.length;//访问length报错,以为number没有length属性
    return something.toString; //不报错,toString是共有属性
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型;

4.对象的类型—接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

定义的变量比接口少、多一些属性是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致。

4.1 可选属性

可选属性的含义是该属性可以不存在,但是仍然不允许添加未定义的属性。

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};

4.2 任意属性

有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

使用 [propName: string] 定义了任意属性取 string 类型的值。

注意

一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集,一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型

4.3 只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

5.数组的类型

//最简单的方法是使用「类型 + 方括号」来表示数组
let fibonacci: number[] = [1, 1, 2, 3, 5];

//数组泛型(Array Generic) Array<elemType> 
let fibonacci: Array<number> = [1, 1, 2, 3, 5];

//接口也可以用来描述数组
interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

//任意类型的数组
ley arr:any=[1,"2",false];

6.函数的类型

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

// 函数声明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

function sum(x: number, y: number): number {
    return x + y;
}

函数表达式定义如下:

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

注意

输入多余的(或者少于要求的)参数,是不被允许的:

我们也可以使用接口的方式来定义一个函数需要符合的形状:

interface SearchFunc {
   (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
    mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
} 

注意

js中的数组、函数同样都是对象,所以接口定义类型同样适它们

6.1 可选参数 

与接口中的可选属性类似,我们用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

注意

可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

6.2 参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制了:

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

6.3 剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):

function push(array, ...items) {
  items.forEach(function(item) {
      array.push(item);
   });
}

let a: any[] = [];
push(a, 1, 2, 3);

事实上,items 是一个数组。所以我们可以用数组的类型来定义它:

function push(array: any[], ...items: any[]) {
   items.forEach(function(item) {
        array.push(item);
   });
}

let a = [];
push(a, 1, 2, 3);

注意,rest 参数只能是最后一个参数,关于 rest 参数,可以参考 ES6 中的 rest 参数。

6.4 重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

7.类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

值 as 类型    /    <类型>值

需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

8.声明文件

//定义了全局变量 jQuery 的类型
declare var jQuery: (selector: string) => any;

jQuery('#foo');

参考:http://ts.xcatliu.com/basics/declaration-files.html

进阶

1.类型别名

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

2.字符串字面量类型

type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}

handleEvent(document.getElementById('hello'), 'scroll');  // 没问题
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'

3.元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

//定义一对值分别为 string 和 number 的元组
let tom: [string, number] = ['Tom', 25];

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

4.枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射

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

编译后:

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

5.类与接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

6.泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

具体概念可以参考java的泛型

7.声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

内置对象

Typescript内置了所有js、dom对象,核心库类型定义文件:https://github.com/Microsoft/TypeScript/tree/main/src/lib

命名空间

使用 namespace 关键字定义命名空间,可以在命名空间内部定义变量、函数表达式、函数声明、接口和 类等值。

为了让命名空间外部可以访问命名空间内部声明的值和类型,使用 export 关键字导出指定的值和类型;

namespace Tools {
  var count = 0
  //导出 add
  export var add = function (x: number, y: number) {
    return x + y
  }
}

引用外部文件的命名空间内的成员时,需要export指定命名空间

exprot namespace {
    let a:any=1;
    export {a}
}

相关总结

类型库:https://github.com/DefinitelyTyped/DefinitelyTyped/

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:npm install @types/node --save-dev

类型别名用来给一个类型起个新名字。type Name = string;

字符串字面量类型,type EventNames = 'click' | 'scroll' | 'mousemove';  使用EventNames 就等于之后的三个类型;

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。let tom: [string, number] = ['Tom', 25];

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等,也可以给枚举项手动赋值:。

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

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

代码检查:http://ts.xcatliu.com/engineering/lint.html

编译选项:http://ts.xcatliu.com/engineering/compiler-options.html

Es6 类

1.属性和方法

class Animal {
    public name;
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        return `My name is ${this.name}`;
    }
}

let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

2.类的继承

使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法

class Cat extends Animal {
  constructor(name) {
    super(name); // 调用父类的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
  }
}

let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom

3.存取器

使用 getter 和 setter 可以改变属性的赋值和读取行为:

class Animal {
  constructor(name) {
    this.name = name;
  }
  get name() {
    return 'Jack';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack

4.静态方法

使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal {
  static isAnimal(a) {
    return a instanceof Animal;
  }
}

let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function

Es7 类

Es7相关提案在Typescript中已实现。

1.实例属性

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal {
  name = 'Jack';

  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Jack

2.静态属性

ES7 提案中,可以使用 static 定义一个静态属性:

class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42

TypeScript 中类的用法 

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

1.抽象类

abstract 用于定义抽象类和其中的抽象方法

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

tsconfig.json

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。

{
  // ...
  "compilerOptions": {
    "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
    "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
    "diagnostics": true, // 打印诊断信息 
    "target": "ES5", // 目标语言的版本
    "module": "CommonJS", // 生成代码的模板标准
    "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
    "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
    "allowJS": true, // 允许编译器编译JS,JSX文件
    "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
    "outDir": "./dist", // 指定输出目录
    "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
    "declaration": true, // 生成声明文件,开启后会自动生成声明文件
    "declarationDir": "./file", // 指定生成声明文件存放目录
    "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
    "sourceMap": true, // 生成目标文件的sourceMap文件
    "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
    "declarationMap": true, // 为声明文件生成sourceMap
    "typeRoots": [], // 声明文件目录,默认时node_modules/@types
    "types": [], // 加载的声明文件包
    "removeComments":true, // 删除注释 
    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
    "noEmitOnError": true, // 发送错误时不输出任何文件
    "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
    "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
    "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
    "strict": true, // 开启所有严格的类型检查
    "alwaysStrict": true, // 在代码中注入'use strict'
    "noImplicitAny": true, // 不允许隐式的any类型
    "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
    "strictFunctionTypes": true, // 不允许函数参数双向协变
    "strictPropertyInitialization": true, // 类的实例属性必须初始化
    "strictBindCallApply": true, // 严格的bind/call/apply检查
    "noImplicitThis": true, // 不允许this有隐式的any类型
    "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
    "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
    "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
    "noImplicitReturns": true, //每个分支都会有返回值
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { // 路径映射,相对于baseUrl
      // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
      "jquery": ["node_modules/jquery/dist/jquery.min.js"]
    },
    "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
    "listEmittedFiles": true, // 打印输出文件
    "listFiles": true// 打印编译的文件(包括引用的声明文件)
  },
   "exclude": [
    "src/lib" // 排除src目录下的lib文件夹下的文件不会编译
  ],
 "files": [
    // 指定编译文件是src目录下的leo.ts文件
    "scr/leo.ts"
  ],
"include": [
    // "scr" // 会编译src目录下的所有文件,包括子目录
    // "scr/*" // 只会编译scr一级目录下的文件
    "scr/*/*" // 只会编译scr二级目录下的文件
  ]
}

相关参考

https://zhuanlan.zhihu.com/p/145210784

声明文件

1.相关语法

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interface 和 type 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// 三斜线指令

2.三斜线指令

三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。

/// <reference path="..." />   #按路径引入
/// <reference types="..." />  #按包名引入

类似于声明文件中的 import,它可以用来导入另一个声明文件。与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:

  • 当我们在书写一个全局变量的声明文件时,在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令
  • 当我们需要依赖一个全局变量的声明文件时,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当也就必须使用三斜线指令来引入
  • 拆分声明文件,当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。

3.d.ts文件

ts 会解析项目中所有的 *.ts 文件、 .d.ts 结尾的文件。所以全局类型声明放在.d.ts中,可直接使用,不需要手动去引入。非全局则需要引入

问题总结

1.通过下标获取对象属性

参考:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types

ts直接通过属性名下标访问对象属性会报错,需要通过keyof处理。