TypeScript类型

TypeScript

什么是TypeScript? TypeScript是由C#语言之父Anders Hejlsberg主导开发的一门编程语言,TypeScript本质上是向JavaScript语言添加了可选的静态类型和基于类的面向对象编程,它相当于是JavaScript的超集,所有现有的 JavaScript 都可以不加改变就在其中使用。它是为大型软件开发而设计的,它最终编译产生JavaScript,所以可以运行在浏览器、Node.js 等等的运行时环境。

ES5、ES6和TypeScript的关系'TypeScript类型'

为何使用Typescript?

  1. TypeScript 增加了代码的可读性和可维护性

  • 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了

  • 可以在编译阶段就发现大部分错误,这总比在运行时候出错好

  • 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等

  1. TypeScript 非常包容

  • TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可

  • 即使不显式的定义类型,也能够自动做出类型推论

  • 可以定义从简单到复杂的几乎一切类型

  • 即使 TypeScript 编译报错,也可以生成 JavaScript 文件

  • 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取

  1. TypeScript 拥有活跃的社区

  • 大部分第三方库都有提供给 TypeScript 的类型定义文件
  • Google 开发的 Angular2 就是使用 TypeScript 编写的
  • TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范

TypeScript 的缺点 任何事物都是有两面性的

  1. 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
  2. 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本
  3. 集成到构建流程需要一些工作量
  4. 可能和一些库结合的不是很完美

typescript除了兼容JavaScript本身就具有的数据类型外,还支持一些它独特的数据类型,所以typescript共支持以下的基本数据类型:

TypeScript基本类型

在TypeScript中有以下基本数据类型

  • 布尔类型(boolean

  • 数字类型(number

  • 字符串类型(string

  • 数组类型(array

  • 元组类型(tuple

  • 枚举类型(enum

  • 任意值类型(any

  • nullundefined

  • void类型   

  • never类型

  • object

一、布尔类型(boolean)

布尔类型是最简单的数据类型,只有truefalse两种值 注意:布尔类型是不能赋予其他值的,如:

let flag: boolean = true;
flag = 1; //报错
二、数字类型(number)

和JavaScript一样,TypeScript数字都是浮点型,也支持二进制、八进制、十进制和十六进制,如:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
三、字符串类型(string)

可以用单引号(')和双引号(")来表示字符串类型,除此之外还支持使用模板字符串反引号(`)来定义多行文本和内嵌表达式。使用${ expr }的形式嵌入变量或表达式,如:

let name: string = 'Angular';
let years: string = 7;
let words: string = `今年是 ${ name } 发布 ${ years } 周年`;
四、数组类型(array)

TypeScript数组的操作类似于JavaScript中数组的操作,TypeScript建议开发者最好只为数组元素赋一种类型的值,定义数组有两种方式,如:

  1. 在元素类型后面加上[]
let arr: number[] = [2,3];
  1. 使用数组泛型
let arr: Array<number> = [2,3];
五、元组类型(tuple)

元组类型用来表示已知数量和类型的数组,各元素的类型不必相同,如:

let x: [string,number];
x = ['Angular',5]; //正确
x = [5,'Angular']; //报错
六、枚举类型(enum)

枚举是一个可被命名的整型常数的集合,枚举类型为集合成员赋予有意义的名称增强可读性,枚举的数据具有有限的可能性如星期、性别等

enum Color {red,green,blue};
let c: Color = Color.blue;
console.log(c); //2

枚举默认下标是0,也可以手动修改,如:

enum Color {red = 2,green = 3,blue = 6};
let c: Color = Color.blue;
console.log(c); //6
七、任意值类型(any)

任意值是TypeScript针对编程时类型不明确的变量使用的一种数据类型,常用于以下三种类型

1、值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。

let x: any = 1;
x = 'I am a string';
x = false;

2、允许你在编译时可选择地包含或移除类型检查

let x: any = 4;
x.toFixed(); //正确,并不检查是否存在

3、定义储存各种类型数据的数组时

let arrarList: any[] = [1,'qwe',true];
八、null和undefined

默认情况下nullundefined是所有类型的子类型。 就是说你可以把null和undefined赋值给number类型的变量。

然而,如果启用--strictNullChecks,就可以使得nullundefined只能被赋值给void或本身对应的类型,如:

let x: number;
x = 1;
x = null; //正确

启用 --strictNullChecks
let y: number;
y = 1;
y = null; //错误
九、void类型

声明一个 void 类型的变量,你只能将它赋值为 undefined 和 null。 使用void表示没有任何类型,例如一个函数没有返回值,意味着返回void,如:

function hello(): void{
    alert('hello Qianmi');
}

let a: void = undefined;
let b: void = null;
十、never类型

never是其他类型(包括nullundefined)的子类型,代表从不会出现的值,这意味着声明为never类型的变量只能被never类型所赋值,在函数中通常表示为抛出异常或无法执行到终止点,如:

let x: never;
let y: number;

//报错
x = 123;

//正确
y = x;
十一、Object

object表示的是 非原始类型,也就是除了number/string/boolean/symbol/null/undefined外的类型。所以可以用Object表示Object.create这样的API,如:

declare function create(o: object | null): void

create({ prop: 0 }) // OK
create(null) // OK
// 以下的则都会报错
create(123)
create('string')
create(false)
create(undefined)

高级类型

一、交叉类型(例子)

交叉类型将多个类型合并为一个类型,相当于新类型具有这么多个类型的所有特性,是一种的操作,交叉类型的形式如:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{}

    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }

    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }

    return result
}

class AdvancedTypesClass {
    constructor(public name: string){}
}

interface LoggerInterface {
    log(): void;
}

class AdvancedTypesLoggerClass implements LoggerInterface {
    log(): void {
        console.log('console logging');
    }
}

var logger = new AdvancedTypesLoggerClass();

var extend1 = extend(new AdvancedTypesClass("string"), new AdvancedTypesLoggerClass());
var e = extend1.name;
console.log(e);//string
extend1.log();//console logging

二、联合类型

联合类型用于限制传入的值的类型只能是|分隔的每个类型,如:number | string | boolean表示一个值的类型只能是number、string、boolean中的一种。 此外,如果一个值是联合类型,那么我们只能访问它们中共有的部分(共有的属性与方法),即相当于一种的关系,如:

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}
function getPet(): Fish | Bird {
    // ...
}
let pet = getPet() // getPet()的返回值类型是`Fish | Bird`
pet.layEggs(); // 允许
pet.swim(); // 报错

Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。

三、类型保护与区分类型

联合类型可以让一个值可以为不同的类型,但随之带来的问题就是访问非共同方法时会报错。那么该如何区分值的具体类型,以及如何访问共有成员?

1、使用类型断言

一个简单的处理方式,是使用类型断言,如:

let pet = getPet()
if ((<Fish>pet).swim) {
    (<Fish>pet).swim()
} else {
    (<Bird>pet).fly()
}
2、使用类型保护

使用类型断言,我们就不得不每次都写一堆的尖括号,很麻烦。那么有没有更好的方式可以判断类型呢?答案是:使用类型保护,如写一个类型判断函数:

function isFish(pet: Bird | Fish): pet is Fish {
    return (<Fish>pet).swim !== undefined
}

这种param is SomeType的形式,就是类型保护,我们可以用它来明确一个联合类型变量的具体类型,在调用时typescript就会将变量缩减为该具体类型,如此一来以下调用就是合法的了:

if (isFish(pet)) {
    pet.swim() // 不会报错
} else {
    pet.fly()
}

允许这么做是因为:typescript能够通过类型保护知道if语句里的pet类型一定是Fish类型,而且else语句里的pet类型一定不是Fish类型,那么就是Bird类型了

3、可为null的类型(例子)

nullundefined可以赋给任何的类型,因为它们是所有其他类型的一个有效值,如:

// 以下语句均合法
let x1: number = null
let x2: string = null
let x3: boolean = null
let x4: undefined = null
let y1: number = undefined
let y2: string = undefined
let y3: boolean = undefined
let y4: null = undefined

在typescript里,我们可以使用--strictNullChecks标记,开启这个标记后,当我们声明一个变量时,就不会自动包含nullundefined,如:

// 开启`--strictNullChecks`后
// Type 'null' is not assignable to type 'number'.
let x1: number = null

// Type 'null' is not assignable to type 'string'.
let x2: string = null

// Type 'null' is not assignable to type 'boolean'.
let x3: boolean = null

// Type 'null' is not assignable to type 'undefined'.
let x4: undefined = null

// Type 'undefined' is not assignable to type 'number'.
let y1: number = undefined

// Type 'undefined' is not assignable to type 'string'.
let y2: string = undefined

// Type 'undefined' is not assignable to type 'boolean'.
let y3: boolean = undefined

// Type 'undefined' is not assignable to type 'null'.
let y4: null = undefined

但是我们可以手动使用联合类型来明确包含,如:

let x = 123
x = null // 报错
let y: number | null = 123
y = null // 允许
y = undefined // 报错,`undefined`不能赋值给`number | null`

当开启了--strictNullChecks可选参数/属性就会被自动地加上| undefined,如:(例子)

function foo(x: number, y?: number) {
    return x + (y || 0)
}
foo(1, 2) // 允许
foo(1) // 允许
foo(1, undefined) // 允许
foo(1, null) // 报错,不允许将null赋值给`number | undefined`类型

四、类型别名

类型别名可以给现有的类型起个新名字,它和接口很像但又不一样,因为类型别名可以作用于原始值、联合类型、元组及其他任何需要手写的了类型,语法如:

type 新名字 = 已有类型

如:type Name = string 别名不会新建一个类型,它只会创建一个新的名字来引用现有类型。所以在VSCode里将鼠标放在别名上时,显示的是所引用的那个类型

1、泛型别名

别名支持泛型,如:

type Container<T> = {
    value: T
}

let name: Container<string> = {
    value: 'RuphiLau'
}

但是类型别名不能出现在声明右侧的任何地方,如:

type Alias = Array<Alias> // 报错,别名Alias循环引用了自身
2、字符串字面量类型

字符串字面量类型允许我们定义一个别名,类型为别名的变量只能取固定的几个值,如:

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
let x1: Easing = 'uneasy' // 报错: Type '"uneasy"' is not assignable to type 'Easing'
let x2: Easing = 'ease-in' // 允许
3、可辨识联合(例子)

可以合并字符串字面量类型联合类型类型保护类型别名来创建可辨识联合的高级模式(也称为标签联合或者代数数据类型),具有3个要素:

  • 具有普通的字符串字面量属性——可辨识的特征
  • 一个类型别名,用来包含了那些类型的联合——联合
  • 此属性上的类型保护 创建一个可辨识联合类型,首先需要声明将要联合的接口,每个接口都要有一个可辨识的特征,如(kind属性):
interface Square {
    kind: 'square'
    size: number
}

interface Rectangle {
    kind: 'rectangle'
    width: number
    height: number
}

interface Circle {
    kind: 'circle'
    radius: number
}

现在,各个接口之间还是没有关联的,所以我们需要使用类型别名来联合这几个接口,如:

type Shape = Square | Rectangle | Circle

现在,使用可辨识联合,如:

function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size
        case 'rectangle':
            return s.height * s.width
        case 'circle':
            return Math.PI * s.radius ** 2
    }
}

五、多态的this类型(例子)

多态的this类型表示的是某个包含类或接口的子类型,例子如:

class jisuan{
    public constructor(protected value: number = 0) {
    }
    public currentValue(): number {
        return this.value
    }
    public jia(operand: number): this {
        this.value += operand
        return this
    }
    public cheng(operand: number): this {
        this.value *= operand
        return this
    }
}

let v = new jisuan(2).cheng(5).jia(1).currentValue() // 11

由于使用了this类型,当子类继承父类的时候,新的类就可以直接使用之前的方法,而不需要做任何的改变,如:

class s extends jisuan {
    public constructor(value = 0) {
        super(value)
    }
    public jian() {
        this.value -= this.value;
        return this
    }
}
let v = new s(2).cheng(5).jian().jia(1).currentValue() //1

如果没有this类型,那么s就不能够在继承jisuan的同时还保持接口的连贯性。因为cheng方法会返回jisuan类型,而jisuan没有jian方法。然而,使用this类型,cheng就会返回this,在这里就是s

六、索引类型

索引类型能使编译器能够检查使用了动态属性名的代码 我们想要完成一个函数,它可以选取对象中的部分元素的值,那么:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n])
}

interface Person {
    name: string
    age: number
}

let p: Person = {
    name: 'RuphiLau',
    age: 21
}

let res = pluck(p, ['name']) // 允许

以上代码解释如下:

  • 首先,使用keyof关键字,它是索引类型查询操作符,它能够获得任何类型T上已知的公共属性名的联合。如例子中,keyof T相当于'name' | 'age'
  • 然后,K extends keyof T表明K的取值限制于'name' | 'age'
  • 而T[K]则代表对象里相应key的元素的类型,所以在例子中,p对象里的name属性,是string类型,所以此时T[K]相当于Person[name],即相当于类型string,所以返回的是string[],所以res的类型为string[] 所以,根据以上例子,举一反三有:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key]
}
let obj = {
    name: 'RuphiLau',
    age: 21,
    male: true
}
let x1 = getProperty(obj, 'name') // 允许,x1的类型为string
let x2 = getProperty(obj, 'age') // 允许,x2的类型为number
let x3 = getProperty(obj, 'male') // 允许,x3的类型为boolean
let x4 = getProperty(obj, 'hobby') // 报错:Argument of type '"hobby"' is not assignable to parameter of type '"name" | "age" | "male"'.

索引类型和字符串索引签名 keyof和T[K]与字符串索引签名进行交互,如果有一个带有字符串索引签名的类型,那么keyof T为string,且T[string]为索引签名的类型,如:

interface Demo<T> {
    [key: string]: T
}
let keys: keyof Demo<boolean> // keys的类型为string
let value: Demo<number>['foo'] // value的类型为number

七、映射类型

我们可能会遇到这么一些需求: 1)将一个现有类型的每个属性都变为可选的,如:

interface Person {
    name: string
    age: number
}

可选版本为:

interface PersonPartial {
    name?: string
    age?: number
}

2)或者将每个属性都变为只读的,如:

interface PersonReadonly {
    readonly name: string
    readonly age: number
}

而现在typescript为我们提供了映射类型,能够使得这种转化更加方便,在映射类型里,新类型将以相同的形式去转换旧类型里每个属性,如以上例子可以改写为:

type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}
type Partial<T> = {
    [P in keyof T]?: T[P]
}
type PersonReadonly = Readonly<Person>
type PersonPartial = Partial<Person>

我们还可以写出更多的通用映射类型,如:

// 可为空类型
type Nullable<T> {
    [P in keyof T]: T[P] | null
}

// 包装一个类型的属性
type Proxy<T> = {
    get(): T
    set(value: T): void
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>
}
function proxify(o: T): Proxify<T> {
    // ...
}
let proxyProps = proxify(props)

由映射类型进行推断(拆包) 上面展示了如何装一个类型,那么与之相反的就有拆包操作,示例如:

function unproxify<T>(t: Proxify<T>): T {
    let result = <T>{}
    for (const k in t) {
        result[k] = t[k].get()
    }
    return result
}
let originalProps = unproxify(proxyProps)

TypeScript类

一、基本用法

可以使用class关键字来声明一个类,类里面可以声明属性方法,如:

class ClassName {
    prop: type // 声明属性
    // 声明构造器
    constructor() {
    }
    // 声明方法
    methodName() {
    }
}

然后可以使用new关键字来实例化一个类,如:let p = new ClassName()

二、继承(例子)

typescript里,同样可以使用常用的面向对象模式,如类的继承,继承使用extends关键字,需要注意的是,一旦子类里显式声明了constructor方法,那么方法内部就必须使用super()方法来调用父类构造器,示例如:

class A {
    name: string;
    constructor(theName: string) { this.name = theName; }
    start(m: number = 0) {
        console.log(`${this.name} 运动了 ${m}米。`);
    }
}

class B extends A {
    constructor(name: string) { super(name); }
    move(m) {
        console.log("B...");
        super.start(m);
    }
}
let b = new B("小明");

b.move(100);//小明 运动了 100米。
三、访问控制(例子)
1、类里面的修饰符public/private/protected
  • public:己,子类中,外部
  • protected:己,子类中
  • private:己
  • 属性不加修饰符,默认是公有
// public修饰符  public:己,子类中,外部
class Person{
    public name:string
    constructor(name:string){
        this.name=name
    }
    run():string{
        return `${this.name}在运动`
    }
}
let p=new Person('李四');
console.log(p.name)
console.log(p.run())
class Web extends Person{
    constructor(name:string){
        super(name)
    }
    work():string{
        return `${this.name}在工作`
    }
}
let web=new Web('成龙')
console.log(web.name)
//protected:己,子类中
class Person{
    protected name:string
    constructor(name:string){
        this.name=name
    }
    run():string{
        return `${this.name}在运动`
    }
}
let p=new Person('李四');
// console.log(p.name)//[ts] 属性“name”受保护,只能在类“Person”及其子类中访问。
// console.log(p.run())


class Web extends Person{
    constructor(name:string){
        super(name)
    }
    work():string{
        return `${this.name}在工作`
    }
}
let web=new Web('成龙')
console.log(web.name)//[ts] 属性“name”受保护,只能在类“Person”及其子类中访问。
console.log(p.name)//[ts] 属性“name”受保护,只能在类“Person”及其子类中访问
//private:己

class Person{
    private name:string
    constructor(name:string){
        this.name=name
    }
    run():string{
        return `${this.name}在运动`
    }
}
let p=new Person('李四');
// console.log(p.name)//[ts] 属性“name”受保护,只能在类“Person”及其子类中访问。
// console.log(p.run())


class Web extends Person{
    constructor(name:string){
        super(name)
    }
    work():string{
        return `${this.name}在工作`//[ts] 属性“name”为私有属性,只能在类“Person”中访问。
    }
}
let web=new Web('成龙')
console.log(web.name)//[ts] 属性“name”为私有属性,只能在类“Person”中访问。
console.log(p.name)//[ts] 属性“name”为私有属性,只能在类“Person”中访问。
2、静态属性

可以使用static来定义类里的静态属性,静态属性属于类自身,而不属于实例,访问的时候要用类名访问,而不能用实例对象访问,如:

class ClassName {
    static propName: string
}
ClassName.propName // 可以访问

let p = new ClassName()
p.propName // 不能访问