丝滑入门TypeScript秘籍

1,001 阅读16分钟

1.认识TypeScript

编程语言分为动态类型语言如JavaScript和静态类型语言如Java。

动态类型语言没有编译阶段,而js本身具有非常大的灵活性,比如它没有类型约束,一个变量可能初始化为字符串,但也可以被赋值为数字,这使得我们用js编程时常常出现意外的错误或者隐患,从而导致运行时错误。

而静态类型语言在编译阶段就能确定每个变量的类型,能够发现大部分的错误。TypeScript就属于静态类型语言。

TypeScript是可扩展的JavaScript。兼容js,具有es6-es10的语法支持,能够兼容各种浏览器,使用ts能够减少代码错误,能够使程序更容易理解和维护(代码即注释), 其代码补全、文件跳转、接口提示等功能能够提高我们的开发效率。此外在编译期间提示代码错误能够帮助我们及时发现一些潜在的bug,甚至一些合法bug。比如:

image.png

虽然使用ts在短期内增加一些开发成本,需要花一些时间写类型,但是对于一些需要团队协助的大型长期维护的项目,还是很有必要的。用着用着就会发现真香。

下面为大家介绍一些TypeScript的基础和常用语法。

2.一些关于TypeScript的小知识

TypeScript 经过编译器 tsc编译和转换后,输出纯js代码。

// 安装tsc
npm install -g typescript
// 编译
// 如果代码有错误,会在编译的时候报错
// 编译完成后会生成一个编译好的hello.js文件
tsc hello.ts
// 运行
node hello.js
  • 为什么 TypeScript 需要一个编译器?

类型注解并不属于 JavaScript(或者专业上所说的 ECMAScript)的内容,所以没有任何浏览器或者运行时能够直接执行不经处理的 TypeScript 代码。它需要经过编译,才能去除或者转换 TypeScript 独有的代码,从而让这些代码可以在浏览器上运行。

  • 报错后仍然可产出文件

如果你希望阻止这一行为,可以在配置文件中开启onEmitOnError编译选项

  • 尽量避免使用any

使用any类型就跟纯JavaScript一样了,就失去了类型检查的意义。启用 noImplicitAny 配置项,在遇到被隐式推断为 any 类型的变量时就会抛出一个错误。

  • strictNullChecks

默认情况下,null 和 undefined 可以被赋值给其它任意类型,但是忘记处理null和undefined会引发一些bug,strictNullChecks 配置项让处理 null 和 undefined 的过程更加明显,不用担心忘记处理

image.png

  • 降级

TypeScript 可以将高版本 ECMAScript 的代码重写为类似 ECMAScript3 或者 ECMAScript5这样较低版本的代码。就是所谓的降级。默认情况下,TypeScript 会转化为 ES3 代码,这是一个非常旧的版本。我们可以使用 target 选项将代码往较新的 ECMAScript 版本转换。例如 --target es2015 参数进行编译,编译后的代码能够在支持 ES5 的环境中执行。

3.基础数据类型

1.布尔值

let isDone:boolean = false

// 注意Boolean是js中的构造函数,
// 使用new Boolean()创造的不是布尔值,而是一个Boolean对象
let createdByNewBoolean: Boolean = new Boolean(1);

let createdByNewBoolean: boolean = new Boolean(1);
// Type 'Boolean' is not assignable to type 'boolean'.

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

2.数值

let age:number = 10

3.字符串

let firstName:string = 'Tom'

4.Null和Undefined

ts中,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量

// 这样不会报错
let num: number = undefined;

5.any

any表示任意类型,可以被改变,也可以访问任意属性和方法,不会报错。常用any表示数组中允许出现任意类型值

变量如果在声明的时候,未指定其类型,那么它会被断言为any类型

let notSure:any = 4
notSure = 'may be a string'

let something  //等价于 let something:any
something = 'seven'
something = 7

6.数组

数组有两种表示方式,1是:类型+方括号, 2是数组泛型 Array<类型>

let arr:number[]=[1,2,3] //数字类型数组,不允许有其他类型值

let arr2: number[] = [1, '1', 2, 3, 5];
// Type 'string' is not assignable to type 'number'.

let arr3: number[] = [1, 2, 3, 5];
arr3.push('8');
// Argument of type '"8"' is not assignable to parameter of type 'number'

//数组泛型 
let arr:Array<number> = [1, 2, 3]

类数组:常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等

function sum() {
    let args: IArguments = arguments;
}

7.元组

数组和元组的区别是,数组只能放一种数据类型的数据,元组可以放多种数据类型的数据。

let user:[string,number]=['Tom',20] //指定了数组的长度和类型
user[0].slice(1)
user[1].toFixed(2)
user.push('123') //只能push两种类型中的一种

4.类型推断

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

let name = 'seven' //等价于 let name:string = "seven'
name = 7 //报错

// 如果定义的时候没有赋值,不管之后有没有赋值,都会推论成any
let name; // 等价于 let name:any;
name = 'seven';
name = 7;

5.联合类型 |

联合类型表示取值可以为多种类型中的一种, 用 | 分隔每个类型

  • 基础类型联合
let numberOrString:number | string
// 允许numberOrString的类型是number或string,但不能是其他类型
numberOrString = 'Seven'
numberOrString = 7

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

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

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

let numberOrString: string | number;
numberOrString = 'seven'; // 推论出string类型
console.log(numberOrString.length); // 5
numberOrString = 7; // 推论出number类型
console.log(numberOrString.length); // 编译时报错
  • 对象类型联合:对象联合类型只能访问联合中所有共同成员
interface Women{
  age: number,
  sex: string,
  cry(): void
}
interface Man{
  age: number,
  sex: string,
}
declare function People(): Women | Man;
let people = People();
people.age = 18; //ok
people.cry();//error 非共同成员

6.类型断言 as

当ts不确定一个联合类型的变量到底是哪个类型时,可以手动指定一个值的类型,从而访问其特有的方法和属性,使用类型断言时要格外小心,注意避免运行时错误。

function getLength(input:string|number):number{
  const str = input as string 
  //使用as关键字做类型断言,告诉IDE,你现在没法判断我的代码,我本人更加清楚,而且你不应该再抛出错误提示
  if(str.length){
     return str.length
  }else{
    const number = input as number
    return number.toString().length
  }
}
//更简洁的写法
if((<string>input).length) {
  return (<string>input).length
} else {
  return input.toString().length
}

7.类型别名 type

就是给类型起个新名字,使用 type 创建,常用于联合类型。

type NameOrResolver = string | number;
const a:NameOrResolver = 2
  • 类型别名和接口的区别?

类型别名和接口非常相似,在大多数情况下你可以自由选择。 几乎所有的 interface 功能都可以在 type 中使用,关键区别在于type类型创建后不能更改,而接口始终是可扩展的,可以向现有接口添加新的字段。

image.png

image.png

8.字符串字面量类型

用来约束取值只能是某几个字符串中的一个

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

9.枚举 enum

用于表述一系列常量,可以双向映射

enum Direction {Up, Left, Down, Right}
console.log(Direction.Up) // 0
// 枚举成员会被赋值为从0开始递增的数字
console.log(Direction[0]) // Up
// 也可以根据枚举值取到枚举名

// 也可以给枚举成员手动赋值,未手动赋值的枚举项会接着上一个递增
enum Direction {Up=3, Left=0, Down, Right}
console.log(Direction.Down) // 1

10.接口 interface

ts中,使用接口来定义对象的类型,用于对(对象的形状)进行描述

interface Person { //接口一般首字母大写
  name:string;
  age:number;
  address?:string; 
  // ?表示可选属性
  readonly id:number; 
  // readonly表示只读属性,初始化对象后不可以重新赋值
  [propName:string]:any; 
  // [propName: string]定义任意属性,
  // 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
}
// tom变量的形状要和接口Person保持一致,多一个属性都不行
let tom:Person = {
  name:'Tom',
  age:20id:898gender:'male'
}

11.内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型.。TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript中的

ECMAScript 标准提供的内置对象有:Boolean、Error、Date、RegExp 等

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

DOM 和 BOM 提供的内置对象有:Document、HTMLElement、Event、NodeList 等

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // ...
});

ts也提供了一些Utilty Types(功能性的类型),在文章的第17章节会详细介绍。

12.交叉类型 &

多种类型的集合,联合对象将具有所联合类型的所有成员

interface People {
  age: number,
  height: number
}
interface Man{
  sex: string
}
const lilei = (man: People & Man) => {
  console.log(man.age)
  console.log(man.height)
  console.log(man.sex)
}
lilei({age: 18,height: 180,sex: 'male'});

13.函数

1.函数声明

// 传参数值类型x和y,并返回数值类型的数据
function sum(x:number,y:number):number {
  return x+y
}
// 输入多余的或少于要求的参数,是不可以的
// 可以用?表示可选参数,可选参数必须接在必需参数后面
function add(x:number,y:number,z?:number):number{
 if(typeof z === 'number'){
   return x+y+z
 }else{
   return x+y
 }
 // 允许给参数添加默认值
 function build(first:string,last:string='hello'){
   return first + last
 }
 let tom = build('Tom')

2.函数表达式

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

3.用接口或type定义函数形状

//interface定义
interface ISum {
 (x:number,y:number):number;
}
let add:ISum = function(x:number,y:number){
  return x+y
}
//也可以用type定义,注意这里用=>而不是:
type ISum = (x:number,y:number)=>number
let add:ISum=function(x:number,y:number){
  return x+y
}
  • never

当一个函数永远也执行不完,如抛错,可以给函数指定never返回值

function throwError(message:string,errorCode:number):never{
  throw {
    message,
    errorcode
  } 
} // 永远也无法执行到这一行

throwError('Not Found',404)
  • 空值

ts中用void表示没有任何返回值的函数

function alert():void{
   alert('hhhaaa')
}

14.类

TypeScript除了实现了所有ES6中的类的功能以外,还添加了一些新的用法

1.修饰符和readonly

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

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
  • 当构造函数修饰为 private 时,该类不允许被继承或者实例化
  • 当构造函数修饰为 protected 时,该类只允许被继承
class Animal {
  public name;
  private age;
  public constructor(name,age) {
    this.name = name;
    this.age = age;
  }
}

let a = new Animal('Jack',28);
console.log(a.name); // Jack
// name被设置为public,可以直接访问实例的name属性
console.log(a.age) //报错
// age被设置为private,不可访问

readonly:只读属性关键字,注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。

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

2.给类加上ts类型

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

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

3.类实现接口

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

interface Alarm {
    alert(): void;
}
interface Light {
    lightOn(): void;
    lightOff(): void;
}

class Door {
  ......
}
class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}
class Car implements Alarm,Light {
    alert() {
       console.log('Car alert');
    },
    lightOn() {
       console.log('Car light on');
    }
    lightOff() {
       console.log('Car light off');
    }
}

4.接口之间的继承

interface Alarm {
    alert(): void;
}
interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}
//LightableAlarm继承了Alarm,
//除了拥有 alert 方法之外,还拥有两个新方法 lightOn和lightOff

15.泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型

1.泛型的基本用法

1.1 泛型在函数中的应用

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
//在函数名后添加了<T>,其中T用来指代任意输入的类型,
//在后面的输入 value: T 和输出 Array<T> 中就可使用了
createArray<string>(3, 'x'); // ['x', 'x', 'x']
// 传入字符串value,输出字符串类型数组
createArray<number>(3, 2); // [2, 2, 2]
// 传入数值value,输出数值类型数组

//可以一次性定义多个类型参数,使用元组的形式
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]

1.2 泛型在类中的应用

class Queue<T>{
  private data = [];
  push(item:T) {
    return this.data.push(item)
  }
  pop():T {
    return this.data.shift()
  }
}
const queue = new Queue<number>()

1.3 泛型在接口中的应用

interface KeyPair<T,U>{
    key:T;
    value:U
}
let kp1:KeyPair<number,string>={key:123,value:'str'}
let kp2:KeyPair<string,number>={key:'string',value:20}

1.4 泛型的综合应用

interface IPlus<T>{
  (a:T,b:T):T
}
function plus(a:number,b:number):number{
    return a+b
}
function connect(a:string,b:string):string{
    return a+b
}
const a:IPlus<number>=plus
const b:IPlus<string>=connect

// 再举个栗子
interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
   ...
}

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

2.泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法,这时,我们可以用extends关键字对泛型进行约束,只允许这个函数传入那些包含特定属性或方法的变量,这就是泛型约束

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity(7) //报错
//如果调用loggingIdentity的时候,传入的arg不包含length,那么在编译阶段就会报错

3.指定泛型参数的默认类型

当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

function createArray<T=string>(length: number, value: T): Array<T> {
   ...
}

16.声明文件 .d.ts

在创建项目的时候,会有很多不是ts书写的库,假如在ts项目中直接使用会出现报错,此时需要一个方式告诉编译器,到底这个库需要什么样的类型即声明文件。声明文件仅提供类型声明,不包含任何业务实现代码。通过声明文件,可以帮助我们在使用第三方库的时候享受ts的便利,自动补全类型提示等。

若本身是ts项目,则不需要书写声明文件,因为经过ts编译后,除了会产生一个js文件,还会生成对应的.d.ts文件。需要单独写的一般是针对原本是js写的模块,但是想在ts项目中使用。

我们使用npm安装的库,其声明文件都会放在node_modules下面的@types文件夹下,ts会默认读取所有的.d.ts文件,所以我们能够在项目任意文件里使用对应的类型定义。

image.png

下面我们学习以下如何书写一个声明文件

首先创建一个.d.ts文件

//test.d.ts
type IOperator='plus'|'minus'
interface ICalculator{
    (operator:IOperator,numbers:number[]):number;
    plus:(numbers:number[]):number;
    minus:(numbers:number[]):number;
}

declare const calculator:ICalculator
export default calculator
//将calculator作为一个模块导出,可以使用import引入

17.常见的Utilty Type

1.Partical<T>

Partical可以将传入的参数的所有属性变成可选的

interface Todo {
   title: string;
   description: string;
}
const myTodo:Partical<Todo>={title:'mytitle'}

2.Required<T>

与Partical相反,Required把传入的参数的所有属性变成必选项

interface Todo {
   title?: string;
   description?: string;
}
const myTodo:Required<Todo>={title:'mytitle'}
//error:Property 'description' is missing in type '{ title: string; }' but required in type 'Required<Todo>'.

3.Readonly<T>

Readonly把传入的参数的所有属性变成只读,可以用于冻结object

interface Todo {
   title: string;
}
const myTodo:Readonly<Todo>={title:'mytitle'}
myTodo.title='hello'
//error:Cannot assign to 'title' because it is a read-only property..

4.Record<Keys, T>

构造一个对象类型,其属性键为keys,属性值为type。可用于将一个类型映射到另一个类型。

interface ICat{
    age:number;
    breed:string;
}
type CatNames='miffy'|'boris'|'lily'
const cats:Record<CatNames,ICat>={
    miffy: { age: 10, breed: "Persian" },
    boris: { age: 5, breed: "Maine Coon" },
    lily: { age: 16, breed: "British Shorthair" },
}
console.log(cats.boris)

5.Pick<T,Keys>

从传入的参数所有属性中选取一组属性Keys来构造类型。

interface Todo{
     title:string;
     description:string;
     completed:boolean;
}
type TodoPerview=Pick<Todo,'title'|'completed'>
const myTodo:TodoPerview={
    title:'clean room',
    completed:true
}

6.Omit<T,Keys>

从传入的参数的所有属性中选取全部,然后删除Keys来构造类型。

interface Todo{
     title:string;
     description:string;
     completed:boolean;
}
type TodoPerview=Omit<Todo,'title'>
const myTodo:TodoPerview={
    description:'clean my room and my brother's room  ',
    completed:true
}

7.Exclude<UnionType, ExcludedMembers>

从传入的联合类型UnionType中,排除一些成员ExcludedMembers,来构造类型

type T0 = Exclude<'a'|'b'|'c','a'>
// 等同于type T0='b'|'c'
type T1=Exclude<'a'|'b'|'c','a'|'b'>
//等同于 type T1='c'

8. Extract<Type,Union>

从传入的Type中,摘录Union中的成员

type T0 = Extract<'a'|'b'|'c','a'|'d'>
//等同于 type T0='a'

9.NonNullable<Type>

从type中排除掉null和undefined

type T0 = NonNullable<string|number|null|undefined>
//等同于 type T0=string|number

更多的UtilityType 移步这里 学习哦

18.配置文件 tsconfig.json

ts使用tsconfig.json文件作为其配置文件,一般存放在根目录下。通常我们可以使用 tsc 命令来编译少量 TS 文件,但如果实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用 tsconfig.json 文件,将需要使用到的配置都写进 tsconfig.json 文件。

下面简要介绍一下几个常见的配置项

{
    "files":[  //指定待编译文件
         "./src/index.ts"
    ],
    "include":[ //和files作用一样,不同的是include支持glob通配符
        "src/**/*" ,//编译src目录的所有文件
    ]
    "exclude":[ //指定编译器需要排除的文件或文件夹,支持glob通配符
        "src/lib", //排除src目录下的lib文件夹下的文件不会编译
    ],
    "compilerOptions": { //配置编译选项
        "outDir":'./output',         // 编译后的文件输出到哪里
        "target": "ES5",             // 目标语言的版本
        "declaration":true,          // 是否生成声明文件
        "declarationDir":"./file",   // 生成的声明文件存放在哪里
        "module": "commonjs",        // 指定生成代码的模板标准
        "noImplicitAny": true,       // 不允许隐式的 any 类型
        "removeComments": true,      // 删除注释 
        "preserveConstEnums": true,  // 保留 const 和 enum 声明
        "sourceMap": true            // 生成目标文件的sourceMap文件
        "strictNullChecks":true,     // 开启null、undefined检测
        "noEmitOnError":true,        // 发现错误时不输出任何js文件
    },
    //引入其他配置文件,继承配置
    "extends":"./tsconfig.base.json",
}

参考 阮一峰TypeScript入门教程TypeScript官网