js typescript基础文档

1 、什么是typescript?
TypeScript是Microsoft(微软)开发的一种开源编程语言,它充分利用了JavaScript原有的对象模型,并在此基础上进行了扩充,所以TypeScript是JavaScript的一个超集,也支持ECMAScript标准。TypeScript设计目标是开发大型应用,它可以编译成纯JavaScript,编译出来的JavaScript可以运行在任何一种JS运行环境中。

相比JS,TS引入了较为严格的类型系统,通过类型注解实现了静态类型检查,面向对象等特性供开发者使用,增强了代码的可读性和可维护性。

2、静态类型 or 动态类型?
通常,编程语言按照类型安全可以分为:强类型和弱类型

按照类型检查可以分为:静态类型 (Static Typing) 和动态类型 (Dynamic Typing)。

静态类型语言会在编译时进行数据类型检查。也就是说,静态类型语言,在编译前,变量的数据类型就确定了,变量值受限于其类型。因为typescript提供了静态类型系统,所以在编译时会进行类型检查。

例如:

let i : number = 10;

在编译前,变量i的数据类型就确定为number了,所以变量值(这里的10)必须归属于number,否则编译时会报错。如下所示:

由于现在的编辑器都很强大,默认通常都内置并启用了Validate,所以,甚至不用到编译时,在编码阶段就会给出语法错误提示。例如:

3、TypeScript的基础类型
TypeScript中的基础类型包括:null、undefined、boolean、number、string、symbol、数组/元祖、enum、any、unknown、void、never

3.1、null/undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。通常在TypeScript项目的根目录下会存在一个tsconfig.json文件,该文件指定了用来编译TypeScript项目的根文件和编译选项。如果在tsconfig.json文件的"compilerOptions"配置项里设置了 “strictNullChecks”: false,则null和undefined是所有类型(never除外)的子类型,也就是说,可以把null和undefined赋值给任何类型(never除外)。

例如:

  const ref: null = null;

  let e:undefined = undefined;

  var m: null = e;

  var n: undefined = ref;

  let a:number = e;

  let b:number = ref;

  let str1:string = ref;

  let str2:string = e;

  ......

上面的代码都可以通过类型检查,但如果修改配置为"strictNullChecks": true,则后面几句都是语法错误,如下:

说明:强烈推荐"strictNullChecks": true设置,否则编程要处处留心‘空’的情况。

3.2、boolean/number/string/symbol
例如:

  let isDone: boolean = false;



  let num: number;

  num = 123;

  num = 0b1111011; // 二进制的123

  num = 0o173;     // 八进制的123

  num = 0x7b;      // 十六进制的123

  

  let name: string =  "smith";

  let info: string = `Hello, my name is ${name}`;

  const s: symbol = Symbol();

 // 在一个整数字面量后加n的方式定义一个BigInt,如:100n 或者调用函数 BigInt():

  const m = 100n;

  const v:BigInt = 100n;   // 这里的BigInt是ecmascript中的接口,非typescript提供的类型注解

  const n = BigInt(100);

  console.log(m === n)  // true

  ......

代码解释:

第 1 行,声明了一个 number 类型最大值的变量 biggest,对于 number 类型来说,这个就是最大精度。

第 3-4 行,最大精度就是这个容器已经完全满了,无论往上加多少都会溢出,所以这两个值是相等的。

注意:在TypeScript中,number和BigInt类型虽然都表示数字,但是实际上两者类型是完全不同的,所以不能互相赋值。

3.3、数组/元祖
TypeScript中数组通常用于放同种类型的元素,当然也可以放不同类型的元素。元祖则表示一个已知元素数量和类型的数组,确切地说,是已知数组中每一个位置上的元素的类型,且各元素的类型不必相同。

数组的两种定义方式:

(1)在元素类型后面接上[],表示由此类型元素组成的一个数组

(2)使用数组泛型,Array<元素类型>

例如:

  let list: number[] = [1, 2, 3];

    let list: Array<number> = [1, 2, 3];
  ......

元组Tuple

例如:

  let arr:[string,number?];

  arr = ['smith'];
  arr = ['smith',18];
  ......

3.4、enum
enum类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。

(1)数字枚举 (双向映射)

例如:

enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

let dir: Direction = Direction.NORTH;
console.log('方向是:',dir)  // 方向是: 0

默认情况下,从0开始为元素编号,如上例,NORTH的初始值为 0,其余的成员的值会自增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。当然也可以手动的指定成员的数值。 例如,我们将上面的例子改成从3开始编号:

例如:

enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST
}

console.log('方向是:',dir)  // 方向是: 3

或者,全部都采用手动赋值:

例如:

  enum Direction{ NORTH = 2, SOUTH = 4, EAST = 6, WEST = 8}

  let dir: Direction = Direction.WEST;
  let dirName: string = Direction[8];

  console.log(dir)   //8
  console.log(dirName) //WEST

(2)字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

例如:

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST"
}

(3)异构枚举

例如:

enum Enum {
    A,
    B,
    C = "C",
    D = "D",
    E = 8,
    F

}

let v1: Enum = Enum.A;  // 0
let v2: Enum = Enum.C;  // C
let v3: Enum = Enum.F;  // 9

3.5、any
在typescript中,any是一种“顶级类型”,也就是说,任何值都可以冠以any类型。如果不希望类型检查器对值进行类型检查而是直接让它们通过编译阶段,那么就可以使用any类型:

例如:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
……

any类型在对老代码进行typescript重构时是非常有用的,它允许你在编译时可选择地包含或移除类型检查。当你只知道一部分数据的类型时,any类型也是有用的。

比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

3.6、unknown
TypeScript 3.0引入的新类型,它是any类型对应的安全类型,也就是说,unknown是一种类型安全的“顶级类型”,任何值都能冠以unknown类型。

例如:

let value: unknown;

value = true;
value = 42;
value = "Hello World";
value = new TypeError();
……

在不想更明确地指定类型或不清楚具体类型时,可使用unknown。通常,直接使用unknown意义不大,但是你可借助“类型守卫”在块级作用域内“收窄”类型,并由此获得准确的类型检查。可以通过不同的方式将unknown 类型缩小为更具体的类型范围,包括 typeof 运算符,instanceof 运算符和自定义类型保护函数等。

例如:

function copy(x:unknown){
  if (typeof x === 'object') {
    return JSON.parse(JSON.stringify(x))
  } else{
    return x
   }
 }

function isNumberArray(value: unknown): value is number[] {
  return (
    Array.isArray(value) &&
    value.every(element => typeof element === "number")
  );

}



const unknownValue: unknown = [15, 23, 8, 4, 42, 16];
if (isNumberArray(unknownValue)) {
  const max = Math.max(...unknownValue);
  console.log(max);
}

unknown和any的主要区别是:unknown是类型安全的,不会破坏类型系统,不能将unknown类型的值直接赋给其它类型(any除外),如:

3.7、void
void类型似乎是与any类型相反,它表示没有任何类型。 在typescript中,如果不关注函数的返回时,通常会将其返回值类型设为void

例如:

function warnUser(): void {
 console.log("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined或null,

例如:

let unusable: void = undefined;
unusable = null;

但如果"strictNullChecks": true,则只能赋予undefined,不能赋予null,如下:

3.8、never
never类型是范畴最小的类型,它是一个空集合,表示永远不存在值的类型,也就是说只要有值就不可能是never类型,所以,任何值都不能冠以never类型。never类型的使用场景:

(1)函数永远无法返回值的情况(例如:死循环、总是报错)

例如:

function infiniteLoop(): never{
 while (true) {}
}

function error(message: string): never{
  throw new Error(message);
}

(2)收窄类型,用于兜底

例如:

function move(direction: "up" | "down") {
  switch (direction) {
      case "up":
       return 1;
      case "down":
       return -1;
  }

  // 用于兜底,这里其实就是隐式使用了never类型
  return Error("永远不应该到这里");
}

说明:never类型其实是很重要的,它使得typescript的类型系统更完善,很多类型函数中,都会用到never类型。

3.9、object
object表示非原始类型,也就是除number、string、boolean、symbol、null或undefined之外的类型。

例如:

let obj: object = { name: "LiLei",age:18};
let fn: object = function (x: number, y: number): number {
  return x + y;
};

4、类型断言
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式,例如:

//(1)“尖括号” 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;


//(2)as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

5、类型别名
TS 允许我们为类型创建一个新名字,这个名字就是类型的别名,然后可以在多处使用这个别名。

类型别名的语法是:“type 名称 = 类型”, 这里的“类型”可以是字面量类型、基础类型、元组、函数、联合类型和交叉类型、甚至还可以是其它类型别名的组合。

例如:

type Str = string;
type ID = number | string;
type Fruit = 'apple' | 'pear' | 'orange' | 'banana'

type PositionType<T> = { x: T, y: T }
  let p1: PositionType<number> = {
    x: 3,
    y: 5
  }

  let p2: PositionType<string> = {
    x: 'right',
    y: 'top'
  }

假如我们有一个获取一个人姓名的函数,它接收一个参数,这个参数有可能直接是要获取的姓名,它是一个 string 类型,也有可能是调用一个函数获取的姓名,它是一个函数类型,我们来看一下这个例子:

例如:

function getName(n) {
  if (typeof n === 'function') {
		return n();
  } else {
    return n;
  }
}

如果要给参数n 进行类型注解,那么它应该是 string | () => string ,即(n:string | () => string),显然这影响代码的可读性,而且string | () => string类型没法复用,所以这时就可以使用类型别名,

例如:

type NameParams = 'string' | () => 'string';

function getName(n: NameParams): string {
  // ... 其它一样
}

对于上面的NameParams类型别名 ,可以进一步拆解,

例如:

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

function getName(n: NameParams): Name {
  // ... 其他一样
}

这里将NameParams 拆成了两个类型别名:Name 和 NameResolver ,分别处理 string 和 () => string 的情况,然后通过联合操作符(|)赋值给NameParams ;还带来了一个优势,函数的返回值很明确就是Name类型,这样Name类型变化,也能很好的反应这个变化。

使用类型别名时也可以在属性中引用自己,例如:

 type Next<T> = {
    val: T,
    next?: Next<T> // 这里属性引用类型别名自身
  }

  let list: Next<string> = {
    val: 'first',
    next: {
      val: 'second',
      next: {
        val: 'third'
      }
    }
  }

6、面向对象
6.1、类
例如:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");
console.log(greeter.greet())  // Hello, world

6.2、继承
例如:

class Animal {
// 默认修饰符是public
    eat(): void {
      console.log("动物的吃方法")
    }
  }

class Dog extends Animal {
    public eat(): void {
      console.log("小狗的吃方法")
      super.eat();
    }
}

  // let dog: Animal = new Dog();
  // 调用的不是注解的类型的方法,而是实际new谁的方法
  let dog: Dog = new Dog();
  dog.eat();

// let dog2: Dog = new Animal();代码明显不合理,在java等语言中,直接就是语法错误,但在ts中可以。
// 原因:TypeScript使用的是结构性类型系统。 
// 当比较两种不同的类型时,并不在乎它们从何处而来
// 对于public成员,如果所有成员的类型是兼容的,就认为它们的类型是兼容的。
  let dog2: Dog = new Animal();
  dog2.eat();

6.3、public/protected/private修饰符
带有private或protected成员的类型,如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员,且它们都是来自同一处声明时,才认为这两个类型是兼容的。

例如:

  class Animal {
    // private是假私有,在外部可以访问到
    private name: string;
    constructor(theName: string) { this.name = theName; }
  }

  class Dog extends Animal {
    constructor() { super("Dog"); }
  }

  class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
  }

  let animal = new Animal("Cat");
  let dog = new Dog();
  // 外部访问private成员
  console.log((dog as any).name);  // Dog
  // Animal和Dog共享了来自Animal里的私有成员定义private name: string
  // 所以Animal和Dog是兼容,下面的赋值语句ok
  animal = dog;
  let employee = new Employee("Bob");
  // animal = employee; // 错误: Animal与 Employee类型不兼容

模拟真私有

6.4、readonly修饰符
你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

例如:

   class Person {
    readonly sex: string = '男';
    readonly name: string;
    constructor(theName: string) {
      this.name = theName;
    }
  }

  let p = new Person("Tom");
  p.name = "dats";  // 错误! name是只读的

简写:

6.5、存取器
TypeScript支持通过getters/setters来截取对对象成员的访问,它能帮助你有效的控制对对象成员的访问。

例如:

  let passcode = "secret passcode";
  class Employee {
    private _age: number;
    get age(): number {
      return this._age;
    }
    set age(newAge: number) {
      if (passcode && passcode == "secret passcode") {
        this._age = newAge;
      }
      else {
        console.log("Error: Unauthorized update of employee!");
      }
    }
  }

  let employee = new Employee();
  employee.age = 30;

对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。
其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

6.6、静态属性
用static修饰的属性即为“静态属性”,静态属性是从属于类的。

例如:

 class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

6.7、抽象类
用abstract修饰的类即为“抽象类”,抽象类通常做为其它派生类的基类使用,它们一般不会直接被实例化。

例如:

 abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象类中的抽象方法不包含具体实现并且必须在非抽象的派生类中实现。 抽象方法的语法与接口方法相似,两者都是定义方法签名但不包含实现。

例如:

abstract class Department {
    constructor(public name: string) {}
    printName(): void {
        console.log('Department name: ' + this.name);
    }
    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {
    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }
    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

6.8、重载
例如:

 class Demo {
    // #region 声明
    public log(): void;
    public log(arg1: string): void;
    public log(arg1: number, arg2: string): void;
    // #endregion

    // 实现
    log(arg1?: string | number, arg2?: string) {
      // 一系列判断确定实现哪个签名的log方法,过于麻烦
      // 没必要向后端看齐,比较扯淡,不建议使用
      // ......
    }
  }

7、接口
在 TypeScript中,接口的作用是:为你的代码或者第三方代码定义类型,约定好契约关系。简单理解,接口就是用来描述对象或者类的具体结构,约束它们的行为。

7.1、怎么定义接口?
和其它语言类似, TypeScript中接口也是使用interface关键字来定义。

例如:

// 定义一个接口Point,这样就获得了一个Point类型。遵照约定,那么Point就必须有x和y属性。
interface Point {
  x: number;
  y: number;
}

7.2、接口的实际使用
(1)可选属性(?)

  interface ISumx {
    x: number;
    y?: number;
  }
  
  function sum({x, y}: ISumx): number {
    return x;
  }
  
  sum({ x: 0 });
  sum({ x: 0, y: 1});

(2)只读属性(readonly)

readonly和const的区别:const是在定义变量的时候使用,而readonly则是定义属性的时候使用。

(3)属性检查

传对象必须使用类型断言才能通过类型检查,如:create({ xx: 0, x: 0, y: 1 } as ICheckPoint)

变量声明类型注解,遵循与上面一样的规则:

(4)约定函数

  interface IFunc {
    sum: (x: number, y: number) => number;
  }
  
  const d: IFunc = {
    sum(x: number, y: number): number{
      return x + y;
    }
  }

上面在接口中定义函数,会约定sum函数接收两个number类型的参数,返回值为number类型。

(5)接口继承

接口继承使用extends关键字,可以让我们更方便灵活的复用。

interface Animal {
  name: string;
  eat(): void;
}

interface Flyable {
  fly(): void;
}

interface Fish {
  swim(): void;
}

interface FlyFish extends Animal, Fish, Flyable{
  move: (m: string) => string
}

(6)实现接口

实现接口使用implement关键字

class Myfish implements FlyFish {
  name: string
  constructor(name: string) {
    this.name = name
  }
  eat(){}
  swim(){}
  fly(){}
  move(name: string) {
    return `${name}在移动`
  }
}

const fish = new Myfish('飞鱼');
console.log(fish.move(fish.name));

(7)接口继承class

(8)其它

// 模拟数组
interface MyArray {
  [index: number]: any;

}

// 模拟对象
interface MyObj {
  [attr: string]: any;
}

// 混合:接口中同时定义属性和函数
interface People{
	(): void;
	name: string;
	age: number;
	sayHi(): void;
}


function getPeople(): People{
	let people: People = (() => {}) as People;
	people.name = 'James';
	people.age = 23;
	people.sayHi = () => { console.log('Hi!') }
  return people;
}


let p = getPeople();
p.sayHi();

7.3、interface vs type
相同点:

(1) 都可以描述一个对象或者函数

// interface
interface User {
 name: string;
 age: number;
}


interface SetUser {
    (name: string, age: number): void;
}



// type
type User = {
  name: string;
  age: number

}

type SetUser = (name: string, age: number): void;

(2)都允许拓展

interface和type都可以拓展,只不过语法不一样而已,并且两者并不是互相独立的,也就是说interface可以extends type, type也可以extends interface。

// 接口同时继承interface和type

// 类型同时拓展于interface和type

因为类可以创建出类型,所以能够在允许使用接口的地方使用类。

不同点

(1)type可以声明基本类型别名、联合类型、元祖等类型

// 基本类型别名
type Name = string;

// 联合类型
interface Dog {wong()}
interface Cat {miao();}
type Pet = Dog | Cat;

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet];

(2)type语句中还可以使用typeof获取实例的类型进行赋值

// 当你想要获取一个变量的类型时,使用typeof
let div = document.createElement('div');
type B = typeof div;

(3)type其它操作

type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];

type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
type T2 = () => void;

// 下面赋值没问题,为啥呢?
const foo: T2 = function(){ return 'aaa';};
const out = foo();
out.length;  // 这里会报错,因为 out 是 void 类型的,而不是string

(4)interface能够声明合并

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


interface User {
 sex: string;
}


/*
合并后的User接口为:
{
  name: string;
  age: number;
  sex: string;

}
*/

总结:interface实现能实现的,type基本也能实现,但官方似乎更钟情于interface,所以,建议能用interface实现,就用interface,否则就用type。

8、高级类型
8.1、各种字面量类型

let a : 10 = 10;
let b : 1 | 2 | 3 | 4 | 5 | 6 = 6;

const str:'foo' = 'foo';
const type:'success' | 'warning'| 'danger' = 'success';
type Easing = "ease-in" | "ease-out" | "ease-in-out";

class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {

        }
        else if (easing === "ease-in-out") {

        }
        else {
         // error! should not pass null or undefined.
        }
    }
}

8.2、交叉类型(Intersection Types)
“T & U”表示交叉类型,其是将多个类型合并为一个类型,感觉称之为“并集类型”更合适。

例如:

interface Button {
  type: string
  text: string

}

interface Link {
  alt?: string
  href: string
}

const linkBtn: Button & Link = { type: 'danger', text: '跳转到百度', href: 'http://www.baidu.com' }
console.log(linkBtn);

8.3、联合类型(Union Types)
“T | U” 表示联合类型,联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个。

例如:

interface Button {
   type: 'default' | 'primary' | 'danger'
   text: string
}

const btn: Button = {
  type: 'primary',
  text: '按钮'
}

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

例如:

interface Bird {
  fly(): void;
  eat(): void;
}

interface Fish {
  swim(): void;
  eat(): void;
}



var pet1: Bird = {
  eat() {
    console.log('鸟吃东西');

  },
  fly() {
    console.log('鸟会飞');
  }

}



var pet2: Fish = {
  eat() {
    console.log('鱼吃东西');
  },
  swim() {
    console.log('鱼会游');
  }
}

function getSmallPet(): Fish | Bird {
    return pet1;
}

let pet3 = getSmallPet();
pet3.eat();      //共有的成员可以访问
// pet3.fly();   // 错误,编译不通过

如果一个值的类型是A | B,就能够确定它包含了A和B中共有的成员。 这个例子里, Bird具有一个fly成员,但我们并不能确定Bird | Fish类型的变量就一定有fly方法,因为如果变量在运行时是Fish类型,那么就不具fly(),这时调用fly()就会出错。

8.4、条件类型(U ? X : Y)
条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。

T extends U ? X : Y    即T是U或者U的子类型,则返回X类型,否则返回Y类型。

例如:

type NonNullable<T> = T extends null | undefined ? never : T;

// 如果泛型参数T为null或undefined,那么取never,否则直接返回T。
let demo1: NonNullable<number>; // => number
let demo2: NonNullable<string>; // => string
let demo3: NonNullable<undefined | null>; // => never



type Diff<T, U> = T extends U ? never : T;  // 从T中去除T和U公共的部分
type Filter<T, U> = T extends U ? T : never;  // 找出T和U公共的部分
type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;

// type T1 = "a" | "c"
type T2 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;

// type T2 = "b" | "d"

8.5、各种内置的类型操作

9、泛型
泛型允许我们在定义函数、接口、type或类的时候先给出类型标记(“类型变量”) 进行类型占位,然后在使用的时候再传递具体的类型给“类型变量”,这样通过“类型变量”就可以实现不同的类型复用同一份代码,所以,泛型是一种创建可复用代码的工具。很多时候我们无法准确定义一个类型,它可以是多种类型,这种情况下我们习惯用 any 来指定它的类型,代表它可以是任意类型。any 虽好用,但是它并不是那么安全的,这时候应该更多考虑泛型。

9.1、泛型函数
例如:

// T类型变量,它是一种特殊的变量,只用于表示类型而不是值,使用<>定义。
// 当arg为一个类型时,T被赋值为这个类型,在返回值中,T即为该类型从而进行类型检查。
function identity<T>(arg: T): T {
  return arg;
}

9.2、泛型接口
例如:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

9.3、泛型类
例如:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

9.4、泛型约束
例如:

interface Lengthwise {
  length: number;
}

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

loggingIdentity({length: 10, value: 3});

9.5、使用泛型来扩展泛型
例如:

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
   (<any>target)[id] = (<any>source)[id];
  }
  return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(x);   // { a: 1, b: 2, c: 3, d: 4 }

let y = copyFields(x, { b: 10, d: 20});
console.log(y); // { a: 1, b: 10, c: 3, d: 20 }

这个描述表示的是,我们接受两个泛型类型的参数用作函数的参数,而第一个类型要被第二个类型所约束,即第二个类型的对象属性必须存在于第一个类型的对象属性列表中。否则就会报错。我们可以将第一个类型看成是子类而第二个类型看成是父类,但是要求并不如继承那样严格罢了。

9.6、在泛型中使用类类型:Using Class Types in Generics
我们可以讲一个类作为类型传入到泛型声明的函数中。所以我们需要对其做一个约束:我们判断传入的类型是否存在一个new的函数,且这个函数返回一个该类型的对象。

例如:

function create<T>(c: {new(): T; }): T {
  return new c();
}

这个代码分为几个部分。接受的参数为c: {new(): T; },表示传入的参数名为c,它有一个名为new()的属性(这个属性恰好就是构造函数),且这个属性的返回值为T,这就决定了传入的c是类类型,即类的类型而不是对象,作为一个参数传入函数中。如果要理解这个模型我们可以借助接口章节的范例来理解。其次,我们定义了返回类型为T的返回值,而在函数体内,我们通过new来新建类型c的对象。

总结:一般来说,在决定是否使用泛型时,应该满足两个标准:
(1)当函数、接口或类要处理多种数据类型时
(2)当函数、接口或类在多个位置使用该数据类型时

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马井堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值