阅读 3469

15分钟入门Typescript

作者:秦志英

前言


在本文开始之前,我们需要先了解一些类型系统的相关知识,然后我们会分析Javascript类型系统的特征和已经存在的一些问题,进而开始学习Javascript类型系统问题的终极解决方案---TypeScript。

一、类型系统

从类型安全的角度出发,类型系统分为强类型与弱类型,从类型检查的角度出发,类型系统分为静态类型与动态类型。

1.1 类型系统分类

强类型

强类型语言是一种强制类型定义的语言,要求变量的使用要严格符合定义,所有的变量都必须先定义后使用,在语言层面就限制变量或者参数的类型,比如函数的实参类型与形参类型必须相同。 优点:

  • 在我们写函数的时候减少不必要的类型判断
  • 在编译阶段就能更早的暴露错误
  • 编码更加准确高效,开发工具可以准确的推断出代码的类型以及它的一些属性和方法
  • 重构已有代码更加牢靠

弱类型

弱类型语言是一种类型可以被忽略的语言,与强类型语言相反,不会在语言层面去限制变量或者参数的类型,比如函数的参数实参与形参类型没有严格的限制。

静态类型

一个变量声明时它的类型是明确的,在它声明过后它的类型不允许再修改。

动态类型

运行阶段才能够明确变量的类型,变量的类型随时可变,在动态语言类型中,它的变量是没有类型的,变量中存放的是值的类型

1.2 Javascript类型系统的特征

由于早期的JavaScript应用过于简单,所以JavaScript语言被设计成弱类型且是动态类型。 优点:

  • 使用起来比较灵活
  • 提升逻辑开发效率,不必花过多的精力关注数据类型问题

缺点:

  • 程序当中的异常只有等到运行的时候才能够发现
  • 类型不明确可能会造成函数的功能发生改变,比如当我们进行两数相加的功能时,传入字符串会变成两个字符串拼接

所有的JavaScript中的类型是灵活多变的,但是也同时丢掉了类型系统的可靠性。在大规模的应用中,JavaScript早期的语言优势也就变成了短板。针对这些问题,出现了JavaScript类型系统的终极解决方案---TypeScript

二、Typescript是什么?

TypeScript是基于JavaScript之上的语言,是JavaScript的超集,它在JavaScript的基础上扩展了一些特性,其实就是一套更强大的类型系统以及对es新特性的支持,用一个公式总结Typescript=Javascript+类型系统+ECMAScript6+

2.1 TypeScript的工作流程

.ts文件最终都会被编译原始的.js文件,我们可以在ts文件中直接使用es6+的新特性,其中的原理和JavaScript中的babel类似,最终都会编译成默认的es3版本代码,当然我们还可以在tsconfig.json中设置对应生成的Javascript版本,具体的TypeScript配置我们会在后面单独讲。

2.1 TypeScript的运行环境搭建

首先你要确保你的电脑已经安装node的运行环境,具体的安装步骤可以到node.js的官网中进行安装,这里就不做过多的演示了,接下来我们演示一下TypeScript的安装步骤。 第一步:先在命令行中安装ts的编辑器

 npm install -g typescript
复制代码

第二步:验证TypeScript是否安装成功, 在终端输入下面命令,如果出现对应的版本号,则安装成功

tsc -v
复制代码

第三步:新建一个test.ts文件

const testFunc = (name) => {
    console.log(`hello-${name}`)
}
复制代码

然后在终端中执行

tsc test.ts
复制代码

最中我们会看到在同级目录下生成对应的js文件,至此TypeScript的环境搭建就完成了,我们可以开始TypeScript的学习了。

三、TypeScript基础类型

从这一章节我们就开始学习TypeScript的相关知识,首先在搭建好的环境内新建一个primitive.ts文件,在这个文件中输入本章节的内容,如果想要看编译结果,可以直接在终端中输入tsc primitive.ts,会在同级目录下生成对应的js文件,如果想要看运行结果,在生成对应的js文件之后,通过执行node primitive.js文件,可以在终端中看到执行的最结果。

如果觉得以上两个步骤太过繁琐,而你又想直接看到运行结果,那么你可以使用ts-node插件解决这个问题。

npm install -g ts-node
复制代码

安装完成之后可以直接在命令后中执行ts-node primitive.ts来查看运行结果。

3.1 布尔值-boolean

let isDone: boolean = false
复制代码

也可以直接使用调用Boolean()返回一个boolean类型

let isDone: boolean = Boolean(true)
复制代码

注意:使用构造函数new Boolean()创建的是一个对象不是布尔值

3.2 数字-number

let num: number = 22
复制代码

除了支持十进制和十六进制字面量之外,TypeScript还支持es6中的二进制和八进制字面量

3.3 字符串-string

let name:string="Bob"
复制代码

3.4 空值-void

JavaScript中没有空值的概念,在TypeScript中void表示没有返回值的函数,void的变量值只能是null和undefined

function log(): void {
    console.log('log......')
}
复制代码

3.5 null和undefined

在TypeScript中可以使用null和undefined来定义对应的两个原始的数据类型,与void不同的是它们还是所有类型的子类型

let undefinedType: undefined = undefined
let nullType: null = null
// 非严格模式下
let num: number = undefined
复制代码

3.6 任意值any

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

let anyType: any = 1234
anyType = 'hello'
复制代码

注意:

  • 1.如果一个变量在声明的时候没有指定其类型,那么它就会被识别为任意值
  • 2.声明一个变量为任意值之后,对它的任何操作,返回内容的类型都是任意值
  • 3.在任意值上面访问任意属性都是被允许的

3.7 unknown类型

所有的数据类型都能够被赋值给unknown类型,但是unknown类型的值只能够被赋值给any类型和unknown类型本身,只有保存任意类型的容器才能够保存unknown类型

let unknownType: unknown = 'hhh'
unknownType = 1233
unknownType = true
复制代码

3.7 never类型

never类型表示那些永远不存在的值的类型。例如,never类型是哪些总是抛出异常或根本不会有返回值的函数表达式的返回值类型;never类型是任何类型的子类型,可以赋值给任何类型;然后没有任何值的类型是never的子类型

function error(message:stirng): never {
	Throw new Error(message)
}
// 永远不会有返回值的类型
function infinite(): never{
	while(true){}
}
复制代码

四、Object类型

Typescript中的Object类型不单单是指普通的对象类型,而是指所有的非原始数据类型。

4.1 Array-数组类型

在TypeScript中可以用两种方式表示数组的类型 第一种: 类型+[]

let arr: number[] = [1,2,3]
复制代码

第二种:Array + < type >

 let arr: Array<number> = [1,2]
复制代码

4.2 Tuple-元祖数据类型

在官方文档中,把元祖数据类型放在基础类型部分,在这里我们把它和数组放在一块讲。元祖类型是一种特殊的数据结构,数据元素是一种明确元素数量以及每一个元素类型的数组。

let tupleType: [string,number] = ['hello',222]
复制代码

4.3 enum-枚举类型

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,枚举类型的特点是: 1.可以给一组数值取一个更容易理解的名字 2.一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性 在JavaScript中可以使用对象来表示枚举类型 但是在TypeScript中可以使用enum关键词实现枚举,如果不指定枚举的值,默认从0开始,也可以使用字符串枚举但是需要指定默认值。

enum statusCode{
    ok=0,
    error=1,
    pedding=2
}
复制代码

编译成Javascript之后的代码

var statusCode;
(function (statusCode) {
    statusCode[statusCode["ok"] = 0] = "ok";
    statusCode[statusCode["error"] = 1] = "error";
    statusCode[statusCode["pedding"] = 2] = "pedding";
})(statusCode || (statusCode = {}));
复制代码

我们可以发现枚举类型会影响我们编译之后(运行时)的代码。通过前面学习的我们会发现TypeScript中的大多数类型在经过编译成JavaScript之后都会被移除掉,这是因为它们的作用就是为了让我们在编译的过程中进行类型检查,但是枚举类型不会,它最终生成了一个双向的键值对对象,既可以通过键取获取值,也可以通过值取获取键,这样就可以通过索引器的方式动态的取获取枚举的名称

4.4 常量枚举

在枚举的前面加上const,枚举类型就会变成常量枚举

const enum statusCode{
    ok=0,
    error=1,
    pedding=2
}
console.log(statusCode.ok)
复制代码

编译成JavaScript之后

console.log(0 /* ok */);
复制代码

我们可以看到我们使用的枚举编译之后会被移除掉,我们在使用枚举值的地方会被替换成具体的数值,在后面会以注释的形式出现我们定义的枚举名称。

4.5 interface-接口

对象字面量

对象类型的限制可以使用类似对象字面量的方式,赋值的对象结构必须和类型结构一致

const obj: {foo: number} = {foo:1}
复制代码

接口

TypeScript为对象提供了一种更为专业的方式---接口。 接口是一个比较抽象的概念,在程序设计里面起到了限制规范的作用,去约定对象成员有哪些属性以及对应的数据类型。

interface Person {
    name: string
    age: number
}
function getPerson(p: Person){
    console.log(p.name)
}
复制代码

编译成JavaScript之后的代码

function getPerson(p) {
    console.log(p.name);
}
复制代码

我们可以看到编译成对应的JavaScript之后对应的接口就不存在了,它的作用就是为了约束对象的结构,在实际的运行阶段没有任何意义。

接口属性

interface User {
    name: string
    // 只读属性
    readonly id: number
    // 可选属性
    age?: number
    // 可索引接口 用来约束数组结构
    books: BooksArr
    // 可索引接口 用来约束对象接口
    message: Message
    // 函数类型接口
    say: Say
}
interface BooksArr {
    // 索引值是number类型 值是string类型的数组
    [index: number]: string 
}
interface Message {
   // key是string类型 value是string或者number类型的对象
    [key: string]: string | number
}
interface Say {
    (str: string) : string
}
复制代码

注意:

  • 赋值的时候,变量的形状必须和接口的保持一致
  • 只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
  • 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型
  • 一旦定义了任意类型属性,那么确定属性和可选属性的类型都必须是它的类型的子集

接口继承

在TypeScript中可以使用extends关键字实现接口继承。

interface Person {
  say(): void
}
interface Student extends Person {
  study(): void
}

class UniversityStudent implements Student {
  say() {}
  study() {}
}
复制代码

类类型接口

接口的另外一个作用就是对类的一部分行为进行抽象。一般来讲一个类只能继承另外一个类,但是有时候不同类之间有一些共有的特性,这个时候就可以把一些共有的特性抽象成一个接口,类可以通过implements关键字来实现这个特性大大提高了面向对象的灵活性。 举个🌰:人和车是属于两个不同的类,但是人和车都有run方法,这个时候我们就可以把run方法提取出来作为一个接口,车和人都去实现它

interface Run {
    Runing(): void
}
class Person  implements Run{
    Runing() {
        console.log('runing slow')
    }
}
class  Car implements Run{
    Runing() {
        console.log('runing slow')
    }
}
复制代码

一个类可以实现多个接口

interface Run {
    Runing(): void
}
interface Eat {
    eating(): void
}
class Person  implements Run, Eat{
    Runing() {
        console.log('runing slow')
    }
    eating() {
        console.log('eating......')
    }
}
复制代码

类与接口之间的区别:

  • 接口只是用于声明成员的方法不做具体的实现,接口在编译成js文件之后就不再存在
  • 类不仅声明并且实现了其方法,在编译之后代码依旧存在

4.6 class-类

类就是用来描述一类具体事物的抽象特征,它的本质还是构造函数。在es5的时候我们可以使用函数+原型去实现一些类,从es6开始JavaScript就有了专门的class,在typescript中又有一些属性增强类class的相关语法,在TypeScript中可以使用三种修饰符修饰类的属性,分别是:

  • public 默认属性,修饰的属性或者方法是公有的,可以在类的内部,类的外部,子类中访问
  • protected 修饰的属性或方法是受保护的,类的内部和子类中可以被访问,类的外部不可以被访问
  • private 私有属性 只能在类的内部中去访问 在外部以及子类中都不能访问

public 默认属性 共有属性

class Animals {
    public name:string
    constructor(name:string){
        this.name = name
    }
    getName(){
        return this.name
    }
}
const animal = new Animals('Catty')
console.log(animal.getName()) // 内部访问
console.log(animal.name) // 外部访问

class Cat extends Animals {
    getParentName(){
        console.log(this.name)
    }
}
const littetCat = new Cat('Catty') 
console.log(littetCat.getParentName()) // 子类实例上访问
复制代码

protected 受保护属性

class Animals {
    protected name:string
    constructor(name:string){
        this.name = name
    }
    getName(){
        return this.name
    }
}
const animal = new Animals('Catty')
console.log(animal.getName()) // 内部访问
console.log(animal.name) // 外部访问 error 属性“name”受保护,只能在类“Animals”及其子类中访问。 

class Cat extends Animals {
    getParentName(){
        console.log(this.name)
    }
}
const littetCat = new Cat('Catty') 
console.log(littetCat.getParentName()) // 子类实例上访问
复制代码

private 私有属性

应用场景一般是不允许外部或者子类修改属性或方法,只允许在类的内部修改属性或者方法的时候使用。

class Animals {
    private name:string
    constructor(name:string){
        this.name = name
    }
    getName(){
        return this.name
    }
}
const animal = new Animals('Catty')
console.log(animal.getName()) // 内部访问
console.log(animal.name) // 外部访问 error 属性“name”为私有属性,只能在类“Animals”中访问。。 

class Cat extends Animals {
    getParentName(){
        console.log(this.name) // error 属性“name”为私有属性,只能在类“Animals”中访问。
    }
}
const littetCat = new Cat('Catty') 
console.log(littetCat.getParentName()) // 子类实例上访问
复制代码

absract 抽象类

抽象类:提供其他类继承的基类,在类的前面加上abstract关键字,这个类就变成了抽象类,抽象类和抽象方法用于定义标准。 首先:抽象类是不允许被实例化的

abstract class Animals {
    public name: string;
    constructor(name: string) {
        this.name = name
    }
    abstract eat():void
}
new Animals('jake') // error 无法创建抽象类的实例。ts(2511)
复制代码

其次:抽象类中的抽象方法必须被子类实现

class Cat extends Animals {
    // 内部必须实现eat方法,否则会报错
    eat(){
        console.log('gulugulu')
    }
}
复制代码

4.7 函数

函数声明

function sum(x:number, y:number):number{
    return x+y
}
复制代码

函数表达式

在TypeScript中的箭头函数用来表示函数的定义,箭头函数的左边是输入类型,右边是输出类型。

let sum:(x:number, y:number) => number = function(x:number, y:number):number{
    return x+y
}
复制代码

用接口定义函数的形状

interface filterKey {
    (Source:string, Key: string):Boolean
}
let TreeSearch : filterKey
TreeSearch = function(Source:string, Key: string):Boolean{
    return Source.indexOf(Key) !== -1
}
复制代码

可选参数

必须放在必选参数的后面,可选参数后面就不允许再出现必选参数了。

function getMessage(name: string, age?:number){
    console.log(name)
}
复制代码

参数默认值

会默认将添加了默认值的参数识别为可选参数,但是不再受后面不能出现必选参数的限制。 function getMessage(name: string, age:number = 18, gender: Boolean){ console.log(name) }

剩余参数

...rest可以获取函数的剩余参数,它只能是最后一个参数

function push(arr: number[], ...rest: any[]){
    console.log(rest) // [2,3]
}
push([1],2,3)
复制代码

重载

允许一个函数接收不同数量或类型的参数时,作出不同的处理,TypeScript会优先从前面的函数定义开始匹配,如果多个函数有包含关系,需要把精确的写在最前面。

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 {
        return x.split('').reverse().join('');
    } 
}
console.log(reverse('abc')) // 'cba'
console.log(reverse(123)) // 321
复制代码

五.泛型

在定义函数、接口或类的时候,有些场景是我们不预先指定类型而是在使用的时候再去指定类型,这个时候我们可以使用泛型。 在函数后面添加,其中T用来指代任意的输入类型,在后面的输入value:T和输出Array中即可使用。

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

泛型约束:在函数内部使用泛型变量的时候,由于事先不知道它是那种类型,所以不能随意操作它的属性或方法 这时候我们可以对泛型进行约束。

interface Lengthwise {
    length: number;
}


function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}
复制代码

六.类型推论

默认类型推导

如果没有明确的指定类型,在TypeScript中会依照类型推论的规则推断出一个类型。

let arr = [1,2,null] // 此时会将arr推断为 let arr: (number | null)[] 
let seven = 7  // 将seven推断为number
seven = 'a' // error 不能将类型“"a"”分配给类型“number”。
复制代码

如果定义时没有被赋值,则会被推断为any类型而不用再受类型检查

let a;
a = 1;
a = 'str'
复制代码

类型推论与内置类型结合

Partial

Partial的作用就是将泛型T中的所有属性都转化成可选属性。

type Partial<T> = {
   [p in keyof T]? : T[p]
 }
 interface Person {
  name: string,
  age: number
}
type t = Partial<Person>; 
/**
 *
 类型推断出来的结果
 type t = {
    name?: string;
    age?: number;
}
 */
复制代码

上面一行代码的作用就是首先通过 keyof 获取 T中的所有属性,然后使用in遍历,并将值赋值给p,后面的?就是将所有的属性都变成可选属性。最后通过T[P]取得相应的属性值。 当我们将定义的接口通过Partial作用之后,将鼠标放置在t上,此时TypeScript就会将t中的数据类型以及结构推断出来。

Required

Required的作用与Partial的作用恰好相反,它的主要作用就是将泛型T中的所有属性都转化为必选属性。

type Required<T> = {
  [P in keyof T]-?: T[P];
};
interface Person {
 name?: string,
 age?: number
}
type t = Required<Person>; 
/**
类型推断出的结果
 * type t = {
    name: string;
    age: number;
}
 */
 const user: t = {
  name: 'jan' // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Required<Person>" 中需要该属性。
}
复制代码

Readonly

将泛型T中的所有属性都转化为只读属性

type Readonly<T> = {
 readonly [P in keyof T]: T[P];
};
interface Person {
name: string,
age?: number
}
type t = Readonly<Person>; 
/**
type t = {
   readonly name: string;
   readonly age?: number;
}
*/
const user: t = {
 name: 'jan' 
}
user.name = 'jery'// 无法分配到 "name" ,因为它是只读属性
user.age = 1 // 无法分配到 "age" ,因为它是只读属性
复制代码

遍历T中的所有属性并将所有属性都变成readonly属性,从上面的age赋值例子我们也可以观察出只读属性的赋值是发生在初始化接口阶段而不是第一个给属性赋值阶段。

ReturnType

获取函数返回值类型。

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

type t = ReturnType<(name: string) => string | number>
// ts推断出 type t = string | number
复制代码

Record

Record的作用是允许从联合类型中创建一个新的类型,联合类型中的值作为新类型的属性。使用场景:接口类型声明。

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

type DepartName = 'grade' | 'office' | 'class'
type DepartList = Record<DepartName, {id: number}>
/**
 type DepartList = {
    grade: {
        id: number;
    };
    office: {
        id: number;
    };
    class: {
        id: number;
    };
 */

const cars: DepartList = {
  grade: {
      id: 1
  },
  office: {
      id: 2
  },
  class: {
      id: 3
  }
}
复制代码

七.联合类型

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

let val: number | string
val = 1;
val = 'aaa'
复制代码

如果一个值是联合类型,那么我们只能访问此联合类型的所有类型的共有成员。

interface Bird {
    fly()
    eat()
}
interface Fish {
    swim()
    eat()
}
function Pets(): Bird | Fish {
    // do something
    return 
}
let p = Pets()
p.eat() // ok
p.fly() // error 类型“Fish”上不存在属性“fly”
复制代码

八.交叉类型

将多个类型合并为一个类型,这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

interface Person {
    name: string
    age: number
}
interface Hobby {
    like: string
}
type Message = Person & Hobby
const Student: Message = {
    name: 'jane',
    age: 18,
    like: 'draw'
}
复制代码

九.类型别名

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

type Messsage = string | string[]
function greet(messsage: Messsage): void{}
复制代码

十.类型断言

可以用来手动指定一个值的类型 因为在ts中我们定义的接口并不是一个真正的类,编译之后接口就不存在了 所有不能instanceOf来判断是联合类型中的哪一个值。

语法1:值 as 类型

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

interface Cat{
    Name: string;
    Run():void
}
interface Fish{
    Name: string;
    Swim(): void
}
function isFish(animal: Fish | Cat){
    if(typeof (animal as Fish).Swim === 'function'){
        return true;
    }
    return false;
}
复制代码

语法2:  <类型> 值

let str: any = 'hello'
let num: number = (<string>str).length
console.log(num)
复制代码

类型断言的特点

  • 联合类型可以被断言为其中的一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为any
  • Any可以被断言为任何类型
  • 要使得A能够被断言为B 只需要A兼容B 或者B兼容A

十一.类型保护

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

in关键字

in操作符可以安全的检查一个对象上是否存在一个属性

interface A {
    num: number
}
interface B {
    str: string
}
function isString(k: A | B): Boolean {
    if('num' in k) {
        return false
    } 
    return true
}
复制代码

typeof

typeof操作符一般检测是否是某一个具体的类型,用于基础类型的检测。

function isString(k: string | number): Boolean {
    if(typeof k === 'string') {
        return true
    } 
    return false
}
console.log(isString('aaa'))
复制代码

instanceof

instanceof操作符是通过构造函数来细化类型的一种方式,原理是通过判断instanceof右侧的构造函数是否出现在左侧的原型链上。

class Fish {
    swim(){
        console.log('swiming in water...')
    }
}
class Bird {
    fly(){
        console.log('flying in sky...')
    }
}
function isFish(arg: Fish | Bird): Boolean {
    if(arg instanceof Fish){
        return true
    }
    return false
}
class GoldFish extends Fish {

}
console.log(isFish(new GoldFish())) // true
复制代码

自定义类型保护

由于Javascript内部并没有丰富的运行时自我检查机制,使用联合类型需要检查是否是其中一个接口的时候,这个时候使用instanceof或者typeof根本无法访问到,因为此时编译成JavaScript的时候这些代码就已经不存在了。这个时候我们需要借助断言将其断言为其中一个来帮助我们缩小范围。

interface Fish {
    swim(): void
    eat?():void
}
interface Bird {
    fly(): void
    eat?(): void
}

function isFish(arg: Fish | Bird): Boolean {
    if( typeof (arg as Fish).swim === 'function') {
        return true
    }
    return false
}
let Fish: Fish = {
    swim(){console.log(1111)}
}
console.log(isFish(Fish))
复制代码

十二.参考资源