TypeScript常用类型

67 阅读25分钟

安装ts

npm i -g typescript

默认情况下,TS会做出下面几种假设:

  1. 假设当前的执行环境是dom(浏览器环境)
  2. 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行
  3. 编译的目标代码是ES3

类型约束

仅需要在 变量、函数的参数、函数的返回值位置加上:类型

let name:string;

name = '222'

function add(a: number, b: number): number {
  return a + b
}

add(3, 2)

ts在很多场景中可以完成类型推导

// 不写返回类型, 也可以根据推导出返回结果
function add(a: number, b: number) {
  return a + b
}

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
  • object: 对象
  • null 和 undefined

null和undefined是所有其他类型的子类型,它们可以赋值给其他类型

通过添加strictNullChecks:true 配置选项,可以获得更严格的空类型检查,null和undefined只能赋值给自身。

// 约束为 number 类型, 返回值会自动推导出 boolean
function isOdd(n: number) {
  return n % 2 === 0;
}

// 写法一 :number[], 表示是一个数字数组, 数组中的每一项必须都是数字类型
let nums: number[] = [3, 4, 5];
// 写法二 :number[], 表示是一个数字数组, 数组中的每一项必须都是数字类型
let nums2: Array<number> = [3, 4, 5];

// 约束为对象 对象里面可以有任意属性
let u:object;
u = {
  a: '1',
  b: 'b'
}

// 对象类型的约束不够精准 因为不能约束对象里面有哪些属性
// 通常用于下面这种场景 只能确定要传递的是一个对象,而不需要确定对象中的结构
function printValues(obj: object) {
  const vals = Object.values(obj);
  vals.forEach((v) => console.log(v));
}
printValues({
  name: 'afd',
  age: 33,
});

// 因为 undefined 可以赋值给 string 类型
let n:string = undefined;
// 导致这里用的时候报错了 所以在开发中, 一般会通过配置 strictNullChecks:true 获得更严格的检查规则
// 配置之后 null和undefined 就只能赋值给自身
n.toUpperCase();

联合类型

多种类型任选其一

配合类型保护进行判断

类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof可以触发类型保护。

// 联合类型 type1 | type2 | typen
let name2: string | undefined;
if(typeof name2 === 'string') {
  // 类型保护
  name.endsWith('.jpg')
}

void 类型

通常用于约束函数的返回值,表示该函数没有任何返回

// void 类型 用于约束函数的返回值,表示该函数没有任何返回
// 当然这里也可以不写 通过类型推导也可以知道
function printMenu(): void {
  console.log('1. 登录');
  console.log('2. 注册');
}

never类型

通常用于约束函数的返回值,表示该函数永远不可能结束

// never类型:通常用于约束函数的返回值,表示该函数永远不可能结束
// 类型推导这里会将返回值推导成 void, 但其实是 never
function throwError(msg: string): never {
  throw new Error(msg);
  console.log('xxx') // 这里不会执行
}

// 类型推导这里会将返回值推导成 void, 但其实是 never
// 这个函数永远不会结束
function alwaysDoSomething(): never {
  while (true) {
    //...
  }
}

字面量类型

使用一个值进行约束

// 字面量类型:使用一个值进行约束
let str_val: 'A';
// 只能赋值为 A
str_val = 'A';

// gender 只能是 男 或 女
let gender: '男' | '女';
gender = '女';
gender = '男';

let arr: []; //arr永远只能取值为一个空数组
arr = []

// 通过字面量约束 user 为一个对象, 而且只有 name 和 age 属性
let user: {
  name: string;
  age: number;
};

user = {
  name: '34',
  age: 33,
};

元祖类型(Tuple)

一个固定长度的数组,并且数组中每一项的类型确定

// tu 必须是数组 而且只能有两项 第一项必须是字符串 第二项必须是数字
let tu: [string, number];
tu = ["3", 4];

any类型

any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型

let data:any = "sfdsdf";
// any 类型可以赋值给任意类型, 下面的代码有隐患
let num:number = data;

类型别名

对已知的一些类型定义名称

type 类型名 = ...
// 约束 u1 是一个对象, 有 name 和 age 属性
let u1: {
  name: string
  age: number
  gender: '男' | '女'
};
// 获取用户 返回的是一个 用户对象数组
function getUsers(): {
  name: string
  age: number
  gender: '男' | '女'
}[] {
  return [];
}
// 可以看到上面 用户对象 类型重复了,而且如果其他地方也要用,需要写很多重复代码
// 如果 用户对象 增加或减少一个属性 需要修改多个地方

// 此时就可以通过 类型别名简化
type Gender = '男' | '女';
type User = {
  name: string;
  age: number;
  gender: Gender;
};

let u2: User;
u2 = {
  name: 'sdfd',
  gender: '男',
  age: 34,
};

function getUsers2(g: Gender): User[] {
  return [];
}

函数的相关约束

目标:函数要实现的目标

两个参数的类型必须相同,如果不同就应该在调用的时候报错提示出来

当传递的参数是 数字时,返回 乘积,返回类型也是 number

当传递的参数是 字符串时,返回拼接结果,返回类型是 string


function combine(a: number | string, b: number | string): number | string {
    if (typeof a === "number" && typeof b === "number") {
        return a * b;
    }
    else if (typeof a === "string" && typeof b === "string") {
        return a + b;
    }
    throw new Error("a和b必须是相同的类型");
}
​
const result = combine("a", 2)

假如有上面的函数,是否符合预期呢

其实是不符合的

首先在函数调用的时候,传递的参数可以不一样,不会报错提示

当传递的参数类型相同时, 返回结果还是会被认为是 number 或 string,而不是准确的类型

要实现上面说要求的,就需要通过函数重载来对函数进行约束。

函数重载:在函数实现之前,对函数调用的多种情况进行声明


/**
 * 得到a*b的结果
 * @param a
 * @param b
 */
function combine(a:number, b:number):number;
/**
 * 得到a和b拼接的结果
 * @param a
 * @param b
 */
function combine(a:string, b:string):string;

有了上面的约束,当调用函数的时候,就必须传递相同的类型, 而且返回值的类型是确定的。

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。


// 可选参数 在参数后面加上问号 可选参数必须在最后面
function sum(a: number, b: number, c?: number) {
  if (c) {
    return a + b + c;
  } else {
    return a + b;
  }
}
sum(3, 4);
sum(3, 4, 5);

默认参数:


function sum2(a: number, b: number, c: number = 2) {
  if (c) {
    return a + b + c;
  } else {
    return a + b;
  }
}
sum2(3, 4);
sum2(3, 4, 5);

枚举

扩展类型:类型别名、枚举、接口、类都是扩展类型

枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到同样的目标。

字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
  • 字面量类型不会进入到编译结果。

let gender3: '男' | '女';
gender3 = '女';
​
function searchUser(g: '男' | '女') {}
// 通过字面量约束类型代码重复了
// 可以通过类型别名解决
​
type Gender = '男' | '女';
let gender3: Gender;
gender3 = '女';
​
function searchUser(g: Gender) {}
​
// 另一个问题
// 上面的代码在对 gender3 赋值的时候 使用的是真实值
// 假如有一天, 表示性别的字面量变了
// 如果代码中有很多这种赋值的地方,那么需要修改的地方就会很多
// 这个问题类型别名解决不了// 也有可能是以下的值
// 先生 女士  男 女   male female
// 虽然上面的每一组值不一样(真实的值是多变的),但是他们的逻辑含义是一样的,都是表示性别
type Gender = '帅哥' | '美女';
gender3 = '美女';

枚举

如何定义一个枚举:


enum 枚举名{
    枚举字段1 = 值1,
    枚举字段2 = 值2,
    ...
}

例如


// 枚举中为什么会出现属性名和属性值 就是为了将逻辑含义和真实值区分开
enum Gender5 {
  male = '男',
  female = '女'
}
let gender5: Gender5;
​
// 有了枚举之后,赋值的时候不能用真实值, 只能用:枚举名.逻辑含义字段
// 这样以后真实值变了 也只需要修改枚举中的真实值
gender5 = Gender5.male
gender5 = Gender5.female;

枚举会出现在编译结果中,编译结果中表现为对象。

枚举的规则:

  • 枚举的字段值可以是字符串或数字

enum Level {
  level1 = 1,
  level2 = 2,
  level3 = 3,
}
​
let l: Level = Level.level1;
l = Level.level2;
​
console.log(l); // 2
  • 数字枚举的值会自动自增

enum Level {
  level1 = 1,
  level2, // 不赋值的时候,会根据上一个值自增 1++ --> 2
  level3,
}
let l: Level = Level.level1;
l = Level.level2;
​
console.log(l); // 2
    
enum Level {
  level1 = 2,
  level2 = 4,
  level3, // 不赋值的时候,会根据上一个值自增 4++ --> 5
}
let l: Level = Level.level1;
l = Level.level3;
​
console.log(l); // 5
​
    
// 都不赋值的时候 第一个默认从 0 开始
enum Level {
  level1, // = 0
  level2, // = 1
  level3, // = 2
}
let l: Level = Level.level1;
l = Level.level2;
​
console.log(l); // 1
​
​
enum Level {
  level1, // 不赋值的时候 第一个默认从 0 开始
  level2 = 4,
  level3, // 不赋值的时候,会根据上一个值自增 4++ --> 5
}
  • 被数字枚举约束的变量,可以直接赋值为数字

// 被数字枚举约束的变量,可以直接赋值为数字
// 但是不建议这样做 因为又是在使用真实值
enum LevelUser {
  level1,
  level2,
  level3,
}
let l_u: LevelUser = 1
l_u  = 2;
  • 数字枚举的编译结果 和 字符串枚举有差异

    具体查看编译结果


// 编译之前
enum Level {
  level1,
  level2,
  level3,
}
let l_u: Level = 1
l_u  = 2;
​
// 编译之后
var Level;
(function (Level) {
    Level[Level["level1"] = 0] = "level1";
    Level[Level["level2"] = 1] = "level2";
    Level[Level["level3"] = 2] = "level3";
})(Level || (Level = {}));
let l_u = 1;
l_u = 2;
​
相当于 生成了下面一个对象
Level = {
    level1: 0,
    level2: 1,
    level3: 2,
    0: 'level1',
    1: 'level2',
    2: 'level3',
}
    
​
// 编译之前
enum Gender5 {
  male = '男',
  female = '女',
}
let gender5: Gender5;
gender5 = Gender5.male;
​
// 编译之后
var Gender5;
(function (Gender5) {
    Gender5["male"] = "\u7537";
    Gender5["female"] = "\u5973";
})(Gender5 || (Gender5 = {}));
let gender5;
gender5 = Gender5.male;
    
相当于 生成了下面一个对象
Gender5 = {
    male: "\u7537", // 编码之后的 '男'
    female: "\u5973", // 编码之后的 '女'
}

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

接口和类型兼容性

扩展类型-接口

接口:inteface

TypeScript的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

和类型别名一样,接口,不出现在编译结果中

接口约束对象


// 通过接口约束对象
interface User5 {
  name: string;
  age: number;
  // sayHello: () => void;
  sayHello(): void; // 两种写法都可以
}
​
// 通过类型别名约束对象
// type User5 = {
//   name: string;
//   age: number;
//   sayHello: () => void;
// };// 在约束对象时 可以用接口也可以用类型别名,两者区别不大,但是建议使用接口
let uu: User5 = {
  name: 'sdfds',
  age: 33,
  sayHello() {
    console.log('asfadasfaf');
  },
};

接口约束函数


// 类型别名约束函数
// type Condition = (n: number) => boolean// 接口约束函数
interface Condition {
  (n: number): boolean;
}
​
// 数组求和 但是要对那些项求和需要使用的时候确定
function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  return s;
}
const result2 = sum([3, 4, 5, 7, 11], (n) => n % 2 !== 0);
console.log(result2);

接口可以继承

可以通过接口之间的继承,实现多种接口的组合


interface A {
  T1: string;
}
​
interface B extends A {
  T2: number;
}
let u4: B = {
  T1: 'abc',
  T2: 23
};
​
​
interface A {
  T1: string;
}
​
interface B {
  T2: number;
}
interface C extends A, B {
  T3: boolean;
}
let u4: C = {
  T1: 'abc',
  T2: 23,
  T3: false,
};

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型


type A = {
  T1: string;
};
​
type B = {
  T2: number;
};
​
type C = {
  T3: boolean;
} & A & B;
​
let u6: C = {
  T2: 33,
  T1: '43',
  T3: true,
};
​

它们的区别:

  • 子接口不能覆盖父接口的成员

interface A {
  T1: string;
}
​
interface B extends A {
  T1: number, // 会报错
  T2: number;
}
  • 交叉类型会把相同成员的类型进行交叉

type B = {
  T2: number;
};
​
type C = {
  T3: boolean;
  T2: string, // 这里不会报错 在赋值的时候可能会
} & B;
​
let u6: C = {
  T2: 33, // T2 会变成 string & number 类型, 这里没办法赋值了
  T3: true,
};

readonly修饰符

只读修饰符,修饰的目标是只读

只读修饰符不在编译结果中


interface User8 {
  readonly id: string // id 是不会变的,这里使用 readonly 进行修饰 避免后面对 id 进行赋值而修改了 id
  age: number,
  name: string,
}
let u8: User8 = {
  id: '123',
  name: 'asd',
  age: 23
}
u8.id = '333' // 不能修改 id, 会报错
​
​
// readonly 修饰数组, 注意修饰的是数组的类型 不是说数组不能重新赋值
// 数组如果不让重新赋值 直接使用 const 就可以了
let arr2: readonly number[] = [3, 4, 6];
arr2 = [9, 10, 23, 45]
​
arr2[0] = 100 // 报错 数组是只读的
// 而且数组中会导致数组被修改一些方法也没有了 比如 push()  // 另一种写法实现只读数组
const arr: ReadonlyArray<number> = [3, 4, 6];
​
​
​
type User9 = {
  readonly id: string;
  name: string;
  age: number;
  // arr 前面的 readonly 表述 arr 属性是只读的, 不能重新进行赋值。 因为在对象里面没办法用 const 来达到不能修改的目的
  readonly arr: string[];
};
​
let u12: User9 = {
  id: '123',
  name: 'Asdf',
  age: 33,
  arr: ['Sdf', 'dfgdfg'],
};
​
u12.arr = [] // 报错 不能对只读属性进行赋值
u12.arr.push('99') // 这样可以 只要不是对 arr 进行重新赋值的操作都可以
​
    
type User9 = {
  readonly id: string;
  name: string;
  age: number;
  // arr 前面的 readonly 表述 arr 属性是只读的, 不能重新进行赋值。 因为在对象里面没办法用 const 来达到不能修改的目的
  readonly arr: readonly string[];
};
​
let u12: User9 = {
  id: '123',
  name: 'Asdf',
  age: 33,
  arr: ['Sdf', 'dfgdfg'],
};
u12.arr = [] // 报错 不能进行重新赋值
u12.arr.push('99') // 报错 数组是只读的

类型兼容性

将B赋值给A,如果能完成赋值,则B和A类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配

  • 对象类型:鸭子辨型法

    • 当直接使用对象字面量赋值的时候,会进行更加严格的判断

// 要将 B 赋值给 A,如果能赋值 说明 B 和 A 的类型兼容
// 判断兼容性的规则
// 如果是普通类型:要求必须完成匹配, 比如字符串类型只能赋值给字符串类型, 不能赋值给数字类型
// 如果是对象类型:采用鸭子辨型法(也叫 子结构辨型法), 目标类型需要某一些特征,赋值的类型只要能满足该特征即可
interface Duck {
  sound: '嘎嘎嘎'; // 字面量类型
  swin(): void;
}
​
let person = {
  name: '伪装成鸭子的人',
  age: 11,
  // 这里使用了 类型断言, 前面的是数据 后面的是类型
  // 在有些时候, ts 的类型推导的结果可能和我们预期的不一样, 而我们有明确的知道这里的类型, 就可以使用类型断言
  // 比如这里 类型推导的结果是 string, 但其实 sound 是字面量类型
  sound: '嘎嘎嘎' as '嘎嘎嘎',
  swin() {
    console.log(this.name + '正在游泳,并发出了' + this.sound + '的声音');
  },
};
​
// 之所以能将 person 人对象赋值给 鸭子对象, 是因为 人对象中的子结构符合鸭子对象的特征
// 人对象中有 sound 字面量, 也有 swin 方法, 就认为他们类型兼容
let duck: Duck = person
// TS 之所以这样设计, 是因为基本类型的区别都比较大, 而对象类型是比较复杂的
// 就比如上面的 person 对象,有可能会用在很多地方,每个地方需要用到的属性可能都不一样
// 如果严格要求类型一致,就会导致需要写多个对象分别来满足不同地方的数据
// 另外考虑到这个数据可能是来自接口返回或者第三方的数据对象 使用者是无法控制返回的对象有哪些属性
// 实际用的时候 不需要那么多的属性 如果都要定义出来就很麻烦,也不通用, 所以采用子结构辨型法可以最大程度的保持 js 的开发习惯
​
​
// 当直接使用对象字面量赋值的时候,会进行更加严格的判断
// 直接赋值 说明是知道这是一个 duck 对象,还写了其它多余的属性, 这种认为是错误
let duck: Duck = { // 报错
  name: '伪装成鸭子的人',
  age: 11,
  sound: '嘎嘎嘎' as '嘎嘎嘎',
  swin() {
    console.log(this.name + '正在游泳,并发出了' + this.sound + '的声音');
  },
};
​
  • 函数类型

    • 一切无比自然

      参数:传递给目标函数的参数可以少,但不可以多

      返回值:要求返回必须返回;不要求返回,你随意;


interface Condition {
  (n: number, i: number): boolean;
}
​
function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  for (let i = 0; i < numbers.length; i++) {
    const n = numbers[i];
    if (callBack(n, i)) {
      s += n;
    }
  }
  return s;
}
// 这里后面的回调函数类型也要符合 Condition
// 参数:传递给目标函数的参数可以少,但不可以多
// 返回值:要求返回必须返回;不要求返回,你随意
// 因为这里只需要一个参数 所以传递一个是可以的
// 要求有返回值 所以必须有返回值
const result9 = sum([3, 4, 5, 7, 11], (n) => n % 2 !== 0);
console.log(result9);

TS中的类

属性

使用属性列表来描述类中的属性


class Person11 {
  constructor(name: string, age: number) {
    // 在 ts 中, 直接这样添加属性是不允许的
    // 这是在动态的创建属性 会导致后续随意增加属性, 造成隐患
    // 在 ts 中,认为一个类有哪些属性 应该是在一开始就能确定的,
    // 所以应该在类中单独的地方以列举的形式将所有的属性列举出来, 而不应该在构造函数中动态创建
    this.name = name;
    this.age = age;
  }
}
​
// 应该写成下面的样子
class Person11 {
  // 通过列表将所有属性列举出来
  name: string
  age: number
  constructor(name: string, age: number) {
    // 在构造函数中初始化
    this.name = name;
    this.age = age;
  }
}
const person = new Person11('abc', 12)
// 在使用 person 时, 也只能使用 name 和 age 属性
console.log(person.age)
person.pId = 'xxx' // 报错 不允许添加属性

属性的初始化检查

strictPropertyInitialization:true


// 当没有写 constructor 构造函数的时候
// 给属性 name 要求的是 string 类型
// 这样就导致创建实例的时候 name 的初始值是 undefined, 和要求的类型不一致
class Person11 {
  name: string
  age: number
}
const person = new Person11()
person.age = 23
person.name = 'aaa' // 虽然这里可以正常赋值
​
​
// 或者写了 constructor, 但是初始化的时候没有对属性(比如 age) 进行初始化赋值
// 导致 age 的值是 undefined
class Person11 {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
  }
}
const person = new Person11('ss', 33)
​
// 上面两种情况都是因为初始化没有做
// 但是 ts 并没有报错, 此时需要配置编译选项, 开启 strictPropertyInitialization
// "strictPropertyInitialization": true // 更加严格的属性初始化检查
// 开启之后,就会报错提示了

属性默认值

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

// gender 属性只有两个值,在创建实例的时候,每次传递三个参数比较麻烦
// 就可以通过设置默认值的方式来处理
class Person11 {
  name: string;
  age: number;
  gender: '男' | '女'; // 应该使用 枚举, 此处是为了说明问题 暂时用字面量代替
  constructor(name: string, age: number, gender: '男' | '女') {
    this.name = name;
    this.age = age;
    this.gender = gender
  }
}
const person = new Person11('ss', 33, '女')
​
​
// 设置默认值的位置有两个地方
class Person11 {
  name: string;
  age: number;
  gender: '男' | '女';
  constructor(name: string, age: number, gender: '男' | '女' = '男') { // 设置默认值
    this.name = name;
    this.age = age;
    this.gender = gender
  }
}
const person = new Person11('ss', 33)
​
// 或者
class Person11 {
  name: string;
  age: number;
  gender: '男' | '女' = '男';  // 设置默认值
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
const person = new Person11('ss', 33)

可选属性和只读属性

属性可以修饰为可选的

属性可以修饰为只读的


class Person11 {
  name: string;
  age: number;
  gender: '男' | '女' = '男'
  pId?: string // 可选属性
  readonly id: string // 只读属性 不允许修改
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.random().toString()
  }
}
const person = new Person11('ss', 33)
​
person.pId = '621028'
person.id = 'axc' // 报错提示

访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

访问修饰符可以修饰类中的任何成员,属性、方法

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:暂时省略

class Person11 {
  name: string;
  age: number;
  gender: '男' | '女' = '男';
  pId?: string; // 可选属性
  readonly id: string; // 只读属性 不允许修改
​
  // 私有属性 不允许外部访问
  private _publishNumber: number = 3; //每天一共可以发布多少篇文章
  private _curNumber: number = 0; //当前可以发布的文章数量
​
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.random().toString();
  }
​
  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log('发布一篇文章:' + title);
      this._curNumber++;
    } else {
      console.log('你今日发布的文章数量已达到上限, 想要发布更多文章, 请升级vip');
    }
  }
}
const person = new Person11('ss', 33)
person.publish('文章1')
person.publish('文章2')
person.publish('文章3')
person.publish('文章4')
person.publish('文章5')

属性简写

这是一个语法糖。

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写


class Person11 {
  age: number;
  gender: '男' | '女' = '男';
  pId?: string; // 可选属性
  readonly id: string; // 只读属性 不允许修改
​
  // name、age 属性是通过构造函数传递进来的 在构造函数中什么也没有做 直接赋值给了对应的属性
  // 符合这种场景的属性可以通过语法糖简写:在构造函数的参数前面加上修饰符, 就会成功对应的属性
  // 比如加上 public 就是公共属性 类外面可以访问
  // 加上 private 属性, 类外面就不能访问了, 加上 readonly 就是只读属性
  // 这里只对 name 使用简写, 便于和 age 做对比
  constructor(public name: string, age: number) {
    this.age = age;
    this.id = Math.random().toString();
  }
}

访问器

作用:用于控制属性的读取和赋值


class Person11 {
  gender: '男' | '女' = '男';
  pId?: string; // 可选属性
  readonly id: string; // 只读属性 不允许修改
​
  // 私有属性 不允许外部访问
  private _publishNumber: number = 3; // 每天一共可以发布多少篇文章
  private _curNumber: number = 0; // 当前可以发布的文章数量
​
  constructor(public name: string, private _age: number) {
    this.id = Math.random().toString();
  }
​
  // 访问器大多情况下会涉及到一个私有属性 注意千万不要写错了
  // 这里千万不能写成 this.age = value, 这种情况会导致无限递归
  // 死循环不一定造成内存泄露 但是无限递归一定会造成内存泄露
  set age(value: number) {
    if (value < 0) {
      this._age = 0;
    } else if (value > 200) {
      this._age = 200;
    } else {
      this._age = value;
    }
  }
​
  // 在写访问器的时候 如果只写了 get 访问器, 那么该属性就相当于是只读属性
  // 不能进行赋值 只是读取的时候可以进行控制
  get age() {
    return Math.floor(this._age);
  }
​
  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log('发布一篇文章:' + title);
      this._curNumber++;
    } else {
      console.log(
        '你今日发布的文章数量已达到上限, 想要发布更多文章, 请升级vip'
      );
    }
  }
}

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)


function take(arr: any[], n: number): any[] {
  if (n > arr.length) {
    return arr;
  }
​
  const result: any[] = [];
  for (let i = 0; i < n; i++) {
    result.push(arr[i]);
  }
  return result;
}
​
const t1 = take([2, 3, 5, 7, 11, 99], 3)
const t2 = take(['aa', 'df', 'gf', 'ty'], 3);
​
// 当我们调用 take 函数的时候, 如果传入的是字符串数组, 返回的就应该是字符串数组
// 也就是说 take 函数中, 三个 any 的地方应该是一致的类型
// 但是在 take 函数中却丢失了这个信息 而且对返回结果进行操作时, 比如循环时, 每一项的信息也是丢失了

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值

在函数中使用泛型

在函数名之后写上<泛型名称>


// 这里的 T 就相当于是一个形参, 只是它代表的是类型
// 具体是什么类型:在声明函数的时候不确定, 只有在调用函数的时候才能确定,就需要传递过来
function take<T>(arr: T[], n: number): T[] {
  if (n > arr.length) {
    return arr;
  }
​
  const result: T[] = [];
  for (let i = 0; i < n; i++) {
    result.push(arr[i]);
  }
  return result;
}
​
// 在调用函数的时候将具体的数组类型传递过去
// 这样返回结果也就能确定为 数字数组
// 在对结果进行处理时 也就会推断出每一项的类型是 number 类型了
const t1 = take<number>([2, 3, 5, 7, 11, 99], 3);
​
// 调用的时候不传递类型, TS 也能智能的根据参数推导出 T 的类型是 string 类型
// 注意: 这是因为参数使用了 T 类型变量, 根据参数推导出 T 是 string, 而 take 函数的 T 和参数中的类型是一样的
// 所以可以确定 T 是 string 类型
const t2 = take(['aa', 'df', 'gf', 'ty'], 3);
​
// 如果参数中没有使用 类型变量, 不传递就推导不了了
// 比如 take 函数 是下面的定义方式
function take<T>(arr: number[], n: number): T[] {}

可以设置默认值


// 设置默认值 不太常用
function take<T = number>(arr: T[], n: number): T[] {}

在类型别名中使用泛型

直接在类型别名后面写上 <泛型名称>


// 用来约束回调函数: 判断数组中的某一项是否满足条件
type callback = (n: number, i: number) => boolean;
// 上面的类型别名存在的问题
// 不通用, 数组的项不一定是 number 类型
// 通过 泛型 改写
type callback<T> = (n: T, i: number) => boolean;
​
function filter<T>(arr: T[], callback: callback<T>): T[] {
  const newArr: T[] = []
  arr.forEach((n, i) => {
    if(callback(n, i)) {
      newArr.push(n)
    }
  })
  return newArr;
}
// 在使用的时候会推导出 T 是 number 类型
filter([2, 3, 5, 6, 9], n => n % 2 !== 0)

在接口中使用泛型

直接在接口名后面写上 <泛型名称>


interface callback<T> {
  (n: T, i: number): boolean;
}
​
function filter<T>(arr: T[], callback: callback<T>): T[] {
  const newArr: T[] = [];
  arr.forEach((n, i) => {
    if (callback(n, i)) {
      newArr.push(n);
    }
  });
  return newArr;
}
// 在使用的时候会推导出 T 是 number 类型
filter([2, 3, 5, 6, 9], (n) => n % 2 !== 0);

在类中使用泛型

直接在类名后面写上 <泛型名称>


// 这个类中提供了一些常见的数组方法
class ArrayHelper {
  // 从数组中取出n 项
  take<T>(arr: T[], n: number): T[] {
    if (n >= arr.length) {
      return arr;
    }
    const result: T[] = [];
    for (let i = 0; i < n; i++) {
      result.push(arr[i]);
    }
    return result;
  }
  // 打乱数组顺序
  shuffle<T>(arr: T[]) {
    for (let i = 0; i < arr.length; i++) {
      const targetIndex = this.getRandom(0, arr.length);
      const temp = arr[i];
      arr[i] = arr[targetIndex];
      arr[targetIndex] = temp;
    }
  }
​
  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + max);
  }
}
​
// 上面这样写有些问题
// take 和 shuffle 方法中的泛型没有产生关联
// 因为调用的时候才传递 泛型,有可能调用的时候传递的不一样
// 但是这是在一个类中, 我们希望在创建数组帮助类的时候就将数组传递过来
// 之后所有的操作都是和传递过来的数组相关的操作export class ArrayHelper<T> {
  constructor(private arr: T[]) {}
​
  take(n: number): T[] {
    if (n >= this.arr.length) {
      return this.arr;
    }
    const newArr: T[] = [];
    for (let i = 0; i < n; i++) {
      newArr.push(this.arr[i]);
    }
    return newArr;
  }
​
  shuffle() {
    for (let i = 0; i < this.arr.length; i++) {
      const targetIndex = this.getRandom(0, this.arr.length);
      const temp = this.arr[i];
      this.arr[i] = this.arr[targetIndex];
      this.arr[targetIndex] = temp;
    }
  }
​
  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + max);
  }
}
const helperArr = new ArrayHelper([2, 4, 5, 7, 0])
const takeArr = helperArr.take(3)

将鼠标定位到方法或者类旁边,按 F12, 可以转到定义的地方。

泛型约束

泛型约束,用于现实泛型的取值


/**
 * 将某个对象的name属性的每个单词的首字母大小,然后将该对象返回
 */
// 因为传入的对象类型和返回的对象类型一致,所以这里可以使用泛型
function nameToUpperCase<T>(obj: T): T {
  // obj.name 会报错
  // 当写 obj.name 的时候,发现没有智能提示, 这是因为 T 可以是任意类型
  // 在这里并不能确定 obj 就是一个对象, 如果是一个对象, 也不能确定里面就有 name 属性
}
nameToUpperCase({
  name: 'zhang san',
  age: 22,
  gender: '男'
})
nameToUpperCase('abcde'); // 比如这里传递的可能是字符串
​
​
// 这就需要对 T 进行约束, 并不是传递什么都可以, 需要符合一定的条件才行
// 这个本示例中, T 必须是一个对象类型, 而且其中必须有 name 属性,值是字符串类型
​
interface hasNameProperty {
  name: string;
}
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
  obj.name = obj.name
    .split(' ')
    .map((s) => s[0].toUpperCase() + s.substr(1))
    .join(' ');
  return obj;
}
nameToUpperCase({
  name: 'zhang san',
  age: 22,
  gender: '男',
});
nameToUpperCase('abcde'); // 会报错

多泛型

泛型可以有多个。


// 将两个数组进行混合
// [1,3,4] + ["a","b","c"] = [1, "a", 3, "b", 4, "c"]
// 为了简单 这里要求数组长度一样
// 因为两个数组的类型可以不一样, 而两个数组的类型在定义的时候也不确定
// 就需要两个泛型来确定
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
  if (arr1.length != arr2.length) {
    throw new Error('两个数组长度不等');
  }
  let result: (T | K)[] = [];
  for (let i = 0; i < arr1.length; i++) {
    result.push(arr1[i]);
    result.push(arr2[i]);
  }
  return result;
}
const result6 = mixinArray([1, 3, 4], ["a", "b", "c"]);
result6.forEach(r => console.log(r));