typescript入门

82 阅读14分钟

Typescript入门

语言类型系统

静态类型与动态类型语言(按类型检查时机分类)

  • 动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误(对于JS是一门解释型语言,没有编译阶段,因此是动态类型)
  • 静态类型是指在编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误(TS是在运行时先编译为JS,在编译阶段就会进行类型检查,因此是静态类型)

强类型与弱类型语言(按是否允许隐式类型转换)

  • JS和TS都是弱类型语言
  • python是强类型语言

虽然TS不限制加号两侧的类型,但是我们可以借助TS提供的类型系统以及ESLint提供的代码检查功能,来限制加号两侧必须同为数字或同为字符串,这一定程度上使得TS向强类型更进一步

这样的类型系统体现了TypeScript的核心设计理念:在完整保留JS运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的bug

基础

原始数据类型

JS的类型分为两种:

  • 原始数据类型(Primitive data types)
    • boolean
    • number
    • string
    • null
    • undefined
    • Symbol(ES6)
    • BigInt(ES10)
  • 对象类型(Object types)

布尔值

TS使用boolean定义布尔值类型

let isDone: boolean = false;	//pass

//注意:使用构造函数Boolean创造的对象不是布尔值
let createdByNewBoolean: boolean = new Boolean(1);	//error

//而是一个Boolean对象
let createdByNewBoolean: Boolean = new Boolean(1);	//pass

//直接调用Boolean可以返回一个boolean类型
let createdByBoolean: boolean = Boolean(1);

数值

使用numbe定义数值类型

let decLiteral: number = 6;
let hexLiteral: numebr = 0xf00d;	//十六进制
let binaryLiteral: number = 0b1010;	//二进制
let octalLiteral: number = 0o744;	八进制
let notANumber: number = NaN;	无效值
let infinityNumber: number = Infinity;	//无穷值

字符串

使用string定义字符串类型

let myName: string = 'Tom';
let myAge: number = 25;

//模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;

空值

JS中没有空值(Void)的概念,在Typescript中,可以用void表示没有任何返回值的函数

function alertNmae(): void {
  alert('My name is Tom');
}

声明一个void类型的变量无用,因为你只能将它赋值为undefined和null

Null和Undefined

与void的区别是,undefined和null是所有类型的子类型(即可以赋值给任意类型),而void类型的变量不能赋值给其他类型

任意值

任意值(Any)用来表示允许赋值为任意类型

任意值的属性和方法

在任意值上访问任何属性都是允许的,也允许调用任何方法(可以认为,一个变量为任意值后,对它的任何操作,返回的内容的类型都是任意值)

未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let something;
//等价于let something: any;
something = 'seven';
something = 3;
something.setName('Tom');

类型推论

若无明确的指定类型,则TS会按类型推论(Type Inference)的规则推断出一个类型

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;//编译时报错

//等价于
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

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

联合类型(Union Types)

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

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

联合类型使用|分隔每个类型

访问联合类型的属性和方法

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

//下面会报错
function getLength(something: string | number): number {
  return something.length;
}

//正常
function getString(something: string | number): string {
  return something.toString();
}

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

let a: string | number;
a = 'seven';
console.log(a.length);
a = 8;
console.log(a.length);//error

对象的类型-接口

在TS中,使用接口(interfaces)来定义对象的类型

什么是接口

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

在TS中,接口除了可用于对类的一部分行为进行抽象外,也常用于对「对象的形状(shape)」进行描述

interface Person{
  name: string;
  age: number;
}
let tom: Person = {
  name: 'Tom',
  age: 12
}

在上面的例子中,我们定义了一个接口Person,接着定义了一个变量tom,它的类型是Person。这样我们就约束了tom的形状必须和接口Person一致

  • 接口一般首字母大写,有的编程语言中会建议接口的名称加上I前缀

  • 定义的变量比接口多了或少了一些属性是不允许的

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性(但仍然不允许添加未定义的属性):

interface Person {
  name: string;
  age?: number;
}
let tom: Person = {
  name:'Tom'
}

任意属性

希望一个接口允许有任意的属性

inferface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}
let tom: Person = {
  name: 'Tom',
  gender: 'male'
}

注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

inferface Person {
  name: string;
  age?: number;
  [propName: string]: string;
}
let tom: Person = {
  name: 'Tom',
  age: 3,
  gender: 'male'
}

一个接口中只能定义一个任意属性,如果接口中有多个类型的属性,可以在任意属性中使用联合类型

inferface Person {
  name: string;
  age?: number;
  [propName: string]: string | number;
}

只读属性

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

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

let tom: Person = {
  id: 89757,
  name: 'Tom',
  gender: 'male'
}
tom.id = 89757;	//error
let tom: Person = {
  name: 'Tom',
  gender: 'male'
}	//error,未对id进行初始化

数组的类型

「类型+方括号」表示法

let fibonacci: number[] = [1, 1, 2, 3, 5];

数组泛型

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

用接口表示数组

interface NumberArray {
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

NumberArray表示:只要索引的类型是数字时,那么值的类型必须是数字

类数组

类数组不是数组类型,比如arguments

function sum() {
  let args: {
    [index: number]: number;
    length: number;
    callee: Function;
  } = arguments;
}
//在这个例子中,我们除了约束当索引的类型是数字时,值的类型也必须是数字之外,也约束了它还有length和callee两个属性

//事实上,常用的类数组都有自己的接口定义,如IArguments, NodeList, HTMLCollection等
function sum() {
  let args: IArguments = arguments;
}
//IArguments是TS中定义好的类型
interface IArguments {
  [index: number]: any;
  length: number;
  callee: Function;
}

any在数组中的应用

用any表示数组中允许出现任意类型

let list: any[] = ['anthony', 23, {website: 'http://anthony.com'}]

函数的类型

函数声明

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

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

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

在TS中需要对其进行约束

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

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

let mySum = function (x: number, y: number): number {
  return x + y;
}
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
  return x + y;
};
  • 在TS的类型定义中:=>用来表示函数的定义,左边是输入类型(需要用括号括起来),右边是输出类型
  • 在ES6中,=>叫箭头函数

用接口定义函数的形状

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

可选参数

可以用?表示可选的参数

//可选参数必须接在必须参数后面
function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + ' ' + lastName;
  } else {
    return firstName;
  }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

参数默认值

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

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

剩余参数

ES6中,可以使用...rest的方式获取函数中的剩余参数(rest参数),rest参数只能是最后一个参数

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

重载

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

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('');
  }
}

类型断言

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

语法

值 as 类型<类型>值,建议统一使用前者

类型断言的用途

将一个联合类型断言为其中一个类型

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,此时可以使用类型断言

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

function isFish(animal: Cat | Fish) {
  if (typeof(animal as Fish).swim) === 'function' {
    return true;
  }
  return false;
}

使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误

将一个父类断言为更加具体的子类
class ApiError extends Error {
  code: number = 0;
}
class HttpError extends Error {
  statusCode: number = 200;
}

function isApiError(error: Error) {
  if(typeof(error as ApiError).code === 'number') {
    return true;
  }
  return false;
}
将任何一个类型断言为any

当我们非常确定这段代码不会出错时,可以使用as any临时将其断言为any类型

(window as any).foo = 1;

它掩盖了真正的类型错误,所以如果不是非常确定,就不要使用as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡

将any断言为一个具体的类型

遇到any类型的变量时,可以选择改进它,通过类型断言及时地把any断言为精确的类型,使我们的代码向着高可维护性的目标发展

function getCacheData(key: string): any{
  return (window as any).cache[key];
}

interface Cat {
  name: string;
  run(): void;
}
const tom = getCacheData('Tom') as Cat;
tom.run();

类型断言的限制

并不是任何一个类型都可以被断言为任何另一个类型

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

let tom: Cat = {
  name: 'Tom',
  run: () => { console.log('run') }
};
let animal: Animal = tom;

TS是结构类型系统,类型之间的对比智慧比较它们最终的结构,而会忽略它们定义时的关系,因此上面与下面等价

interface Animal {
  name: string;
}
interface Cat extends Animal {
  run(): void;
}

即Animal兼容Cat

要使得A能够被断言为B,只需要A兼容B或B兼容A即可,这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为any
  • any可以被断言为任何类型
  • 要使得A能够被断言为B,只需要A兼容B或B兼容A即可(前面四种情况都是最后一个的特例)

双重断言

可以使用as any as Foo来将任何一个类型断言为任何一个类型

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

function testCat(cat: Cat) {
  return (cat as any as Fish);
}

若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。

除非迫不得已,千万别用双重断言。

类型断言 VS 类型转换

类型断言只会影响TS编译时的类型,类型断言语句在编译结果中会被删除,因此类型断言不是类型转换,不会真的影响到变量的类型

类型断言 VS类型声明

共同的作用

function getCacheData(key: string): any {
  return (window as any).cache[key];
}

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

const tom = getCacheData('tom') as Cat;
//const tom: cat = getCacheData('tom');
tom.run();

区别

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

const animal: Animal = {
  name: 'tom'
};
let tom = animal as Cat;	//pass
let tom: Cat = animal; //error

核心区别

  • animal断言为Cat,只需要满足Animal兼容Cat或Cat兼容Animal即可
  • animal赋值给Cat,需要满足cat兼容Animal才行

类型声明比类型断言更加严格,最好优先使用类型声明

类型断言 VS 泛型

function getCacheData(key: string): any {
  return (window as any).cache[key];
}

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

const tom = getCacheData('tom') as Cat;
tom.run();

也可以使用泛型来解决

function getCacheData<T>(key: string): T {
  return (window as any).cache[key];
}

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

const tom = getCacheData('tom') as Cat;
//const tom: cat = getCacheData<Cat>('tom');
tom.run();

通过给getCacheData函数添加了一个泛型,可以更加规范的实现对getCacheData返回值的约束,也同时去掉了代码中的any,是最优的解决方案

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能

新语法索引

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

什么是声明语句

在html通过

$('#foo');
//or
jQuery('#foo');

在ts中,编译器不知道$或jQuery是什么,这时需要使用declare var来定义它的类型

declare var jQuery: (selector: string) => any;	//并没有真的定义变量,只是定义了全局变量jQuery的类型,仅仅用于编译时的检查,并在编译结果中会被删除

jQuery('#foo');

什么是声明文件

把声明语句放到单独的文件中,则为声明文件,声明文件必须以.d.ts为后缀,对于第三方库的声明文件,推荐使用@types统一管理,使用方式是直接用npm安装对应的声明模块即可

书写声明文件

库的使用场景

  • 全局变量:通过<script>标签引入第三方库,注入全局变量
  • npm包:通过import foo from 'foo'导入,符合ES6模块规范
  • UMD包:既可以通过<script>标签引入,也可以通过import导入
  • 直接扩展全局变量:通过<script>标签引入,改变一个全局变量的结构
  • 在npm包或UMD库中扩展全局变量:引用npm包或UMD库后,改变一个全局变量的结构
  • 模块插件:通过<script>import导入后,改变一个模块的结构
全局变量

全局变量的声明文件主要有以下几种语法

declare var 声明全局变量

一般来说,全局变量都是禁止修改的常量,因此大部分情况应该使用const而不是var或let

同时,声明语句中只能定义类型,切勿在声明语句中定义具体的实现

declare function

用来定义全局函数,支持函数重载

declare class
declare calss Animal {
  name: string;
  constructor(name: string);
  sayHi(): string;
}
declare enum
declare enum Directions {
  Up,
  Down,
  Left,
  Right
}
declare namespace
npm包

npm包的声明文件可能存在两个地方

  • 与该npm包绑定在一起
  • 发布到@types

若上述两种方式都没有找到对应的声明文件,则需要自己为其写声明文件,声明文件存放的位置有两种方案

  • 创建一个node_modules/@types/foo/index.d.ts文件,存放foo模块的声明文件
  • 创建一个types目录,专门用来管理自己写的声明文件,将foo的声明文件放到types/foo/index.d.ts中,这种方式需要配置下tsconfig.json中的paths和baseUrl字段

目录结构

/path/to/project
├── src
|  └── index.ts
├── types
|  └── foo
|     └── index.d.ts
└── tsconfig.json

tsconfig.json内容

{
  "compilerOptions":{
    "module": "commonjs",
    "baseUrl": "./",
    "paths": {
      "*": ["types/*"]
    }
  }
}

npm包的声明文件语法

  • export 导出变量
  • export namespace 导出(含有子属性)对象
  • export default ES6默认导出
  • export = commonjs导出模块

内置对象

ECMAScript的内置对象:Boolean、Error、Date、RegExp等

DOM和BOM的内置对象:Document、HTMLElement、Event、NodeList