安装ts
npm i -g typescript
默认情况下,TS会做出下面几种假设:
- 假设当前的执行环境是dom(浏览器环境)
- 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行
- 编译的目标代码是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 // 更加严格的属性初始化检查
// 开启之后,就会报错提示了
属性默认值
属性的初始化位置:
- 构造函数中
- 属性默认值
// 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));