数据类型
基本数据类型
boolean、number、string、symbol、null、undefined
let age: number = 18
let myName: string = '老师'
let isLoading: boolean = false
引用数据类型
普通对象
JS 中的对象是由属性和方法构成的,而 TS 对象的类型就是在描述对象的结构(有什么类型的属性和方法)
let person: {
name: string
sayHi(): void
} = {
name: 'jack',
sayHi(content: string) {}
}
- 也可以用类型别名替换
type Person = {
name: string,
age: number,
girlFriend?: string, // ? 表示可选属性
// sayHi: (content: string) => void
sayHi(content: string): void
}
// ts 就像在写注释, 以前写的注释是给程序员看的, ts 写的类型是给编辑器看的, 程序员也可以看
let obj1: Person = {
name: 'james',
age: 39,
sayHi(content) {
console.log(content)
}
}
数组
两种写法
// 写法一(推荐):
let numbers: number[] = [1, 3, 5]
// 写法二:
let strings: Array<string> = ['a', 'b', 'c']
函数
函数的类型实际上指的是: 函数参数 和 返回值 的类型
两种方式:
- 单独指定参数、返回值的类型
// 函数声明
// function 函数名(参数1: 参数1类型, 参数2: 参数2类型): 返回值类型 { 函数体 }
function add(a: number, b: number): number {
return a + b
}
// 函数表达式
const fn = function(a: number, b: number): number {
return a + b
}
// 箭头函数
// 注意事项: 以前箭头函数如果只有一个参数, 则可省略小括号, ts不行
// ts箭头函数必须要有小括号
const sub = (a: number): number => {
return a
}
const sub = (a: number, b: number): number => {
return a - b
}
- 同时指定参数、返回值的类型(函数的类型别名)
type AddFn = (num1: number, num2: number) => number
const add: AddFn = (num1, num2) => {
return num1 + num2
}
元组类型
元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型。
let position: [number, number] = [39.5427, 116.2317]
- 元组类型可以确切地标记出有多少个元素,以及每个元素的类型
- 该示例中,元素有两个元素,每个元素的类型都是 number
使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字
特殊类型
特殊类型包括:any、unknow、void、nerver、Enum(枚举)
any
在 TS 中,任何类型都可以归于 any 类型,所以any类型也就成了所有类型的顶级类型,同时,如果不指定变量的类型,则默认为any类型, 当然不推荐使用该类型,因为这样丧失了TS的作用
let d:any; //等价于 let d
d = '1';
d = 2;
d = true;
d = [1, 2, 3];
d = {}
unknow
与any一样,都可以作为所有类型的顶级类型,但 unknow更加严格,那么可以说除了any 之下的第二大类型,接下来对比下any,主要严格于一下两点:
void类型
如果函数没有返回值,那么,函数返回值类型为: void
// 如果什么都不写,此时,add 函数的返回值类型为: void
const add = () => {}
// 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同
const add = (): void => {}
// 但如果指定返回值类型为 undefined 时,函数体中必须显式的 return undefined 才可以
const add = (): undefined => {
// 此处,返回的 undefined 是 JS 中的一个值
return undefined
}
never
表示一个函数永远不存在返回值,TS会认为类型为 never,那么与 void 相比, never应该是 void子集, 因为 void实际上的返回值为 undefined,而 never 连 undefined也不行
- 符合never的情况有:当抛出异常的情况和无限死循环
枚举类型
枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个
在TypeScript中使用枚举,是通过 enum 关键字来进行定义的
基本用法
默认情况下,枚举成员的值是从 0 开始递增的。
// 创建枚举
enum Direction {
Up,
Down,
Left,
Right,
}
//Direction 枚举定义了四个常量值:Up、Down、Left、Right。
默认情况下,枚举成员的值是从 0 开始递增的。因此,Direction.Up 的值为 0,Direction.Down 的值为 1,以此类推。
// 使用枚举类型
function changeDirection(direction: Direction) {
console.log(direction)
}
// 调用函数时,需要应该传入:枚举 Direction 成员的任意一个
// 类似于 JS 中的对象,直接通过 点(.)语法 访问枚举的成员
changeDirection(Direction.Up)
数字枚举
默认为从 0 开始自增的数值,为第一个成员设置数值后,则从该值开始递增
enum Enum {
A,
B,
C = 4,
D,
E = 8,
F,
}
也为每一个成员都设置了数值
enum Direction { Up = 10, Down=13, Left=15, Right=20 }
字符串枚举
字符串枚举的每个成员必须有初始值(没有自增长行为)
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
布尔类型枚举
布尔类型枚举,顾名思义就是定义枚举类型值为布尔的 定义方法如下
enum BooleanEnum {
TRUE = true,
FALSE = false
}
异构枚举
实际项目中 也有可能会遇到一个枚举里面需要字符串、数字、布尔类型 同时存在的,这就是异构枚举
enum MixedEnum {
A = 1,
B = "b",
C = "c",
D = true
}
接口枚举
在实际的项目中 我们可能在接口的定义中 某个属性会使用到枚举。例如我们接口中有个方向属性 它可能为枚举中的任意值,如下代码定义
//定义方向枚举
enum Direction {
Up = 'up',
Down = 'down',
Left = 'left',
Right = 'right',
}
//定义接口
interface MyInterface {
a:1
direction : Direction //方向的定义
}
const枚举
在 TypeScript 中,const 枚举和普通枚举有以下几个区别:
- 编译结果:普通枚举在编译后会生成一个真实的对象,而 const 枚举在编译后会直接将枚举的值内联到使用处,不会生成真实的对象。
- 使用方式:普通枚举可以通过枚举成员的名称进行访问,而 const 枚举只能通过值进行访问。
- 只读性质:const 枚举成员是只读的,不允许修改其值,而普通枚举成员是可变的。
- 枚举语句:普通枚举可以包含成员的初始化表达式,而 const 枚举不允许包含任何初始化表达式。
const enum enumDirection {
up,
down
}
console.log("direction", enumDirection.up)
如果是普通枚举:
由于 const 枚举的编译结果会将枚举的值内联到使用处,因此适用于编译后不需要真实对象的场景,可以减少编译结果的体积。而普通枚举则适用于需要在运行时访问枚举成员以及进行修改的场景。
枚举是 TS 为数不多的非 JavaScript 类型级扩展(不仅仅是类型)的特性之一
因为:其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
也就是说,其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
// 会被编译为以下 JS 代码:
var Direction;
(function (Direction) {
Direction['Up'] = 'UP'
Direction['Down'] = 'DOWN'
Direction['Left'] = 'LEFT'
Direction['Right'] = 'RIGHT'
})(Direction || Direction = {})
使用枚举
枚举成员可以直接通过枚举类型来访问,也可以通过枚举的值来访问
let playerDirection: Direction = Direction.Up;
console.log(playerDirection); // 输出:1
常用枚举操作:
- 获取枚举成员的数量:Object.keys(Direction).length
- 获取枚举成员的名称:Direction[1] 返回 Up
其他类型
其他类型包括:类型推理、字面量类型、联合类型、交叉类型
类型推论
在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型
// 变量 age 的类型被自动推断为:number
let age = 18
// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number): number {
return num1 + num2
}
- 发生类型推论的 2 种常见场景:
- 声明变量并初始化时
- 决定函数返回值时
字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型。对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型
let str: 'hello world' = 'hello world';
let num: 996 = 996;
let bool: true = true
字面量类型的使用
字符串字面量
字符串字面量类型其实就是字符串常量,与字符串类型不同的是它是具体的值
type Name = "TS";
const name1: Name = "test"; // error 不能将类型"test"分配给类型"TS"
const name2: Name = "TS";
实际上,定义单个的字面量类型并没有太大用处,它的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合:
type Direction = "north" | "east" | "south" | "west";
function getDirectionFirstLetter(direction: Direction) {
return direction.substr(0, 1);
}
getDirectionFirstLetter("test"); // error 类型"test"的参数不能赋给类型“Direction”的参数
getDirectionFirstLetter("east");
这里我们使用四个字符串字面量类型组合成了一个联合类型,这样编译器就会检查我们使用的参数是否是指定的字面量类型集合中的成员。通过这种方式,可以将函数的参数限定为更具体的类型。这不仅提升了代码的可读性,还保证了函数的参数类型
数字字面量
数字字面量类型和字符串字面量类型差不多,都是指定类型为具体的值:
type Age = 18;
interface Info {
name: string;
age: Age;
}
const info: Info = {
name: "TS",
age: 28 // error 不能将类型“28”分配给类型“18”
};
布尔字面量
布尔字面量和上面的两个类似,不在多说:
let success: true
let fail: false
let value: true | false
由于布尔值只有true和false两种,所以以下两种类型意思一样的:
let value: true | false
联合类型
如果希望属性为多种类型之一,如字符串或者数组,这时联合类型就派上用场了(它使用 | 作为标记,如 string | number)。
联合类型可以理解为多个类型的并集。联合类型用来表示变量、参数的类型不是某个单一的类型,而可能是多种不同的类型的组合
let arr: (number | string)[] = [1, 2, 3, 'abc']
// 注意事项: | 的优先级较低, 需要用 () 包裹提升优先级
// 一旦使用联合类型, 说明 arr 中存储的既可能是 number 也可能是 string, 所以会丢失一部分提示信息 (只能提示共有的方法和属性)
let timerId: number | null = null
- 注意:
|(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种。这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(||)混淆了
类型保护
当我们使用联合类型时,如果我们希望把当前值的类型收窄为当前值的实际类型,而类型保护就是实现类型收窄的一种手段
型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护
in 关键字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
typeof 关键字
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof 关键字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
自定义类型保护的类型谓词(type predicate)
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
自定义类型保护
我们以车辆和汽车的例子为例,来创建一个自定义类型保护函数 —— isCar,它的具体实现如下:
function isCar(vehicle: any): vehicle is Car {
return (vehicle as Car).turnSteeringWheel !== undefined;
}
你可以传递任何值给 isCar 函数,用来判断它是不是一辆车。isCar 函数与普通函数的最大区别是,该函数的返回类型是 vehicle is Car,这就是我们前面所说的 “类型谓词”。
在 isCar 函数的方法体中,我们不仅要检查 vehicle 变量是否含有 turnSteeringWheel 属性,而且还要告诉 TS 编译器,如果上述逻辑语句的返回结果是 true,那么当前判断的 vehicle 变量值的类型是 Car 类型。
现在让我们来重构一下前面的条件语句:
// vehicle instanceof Car -> isCar(anotherCar)
if (isCar(anotherCar)) {
anotherCar.turnSteeringWheel('left');
console.log("这是一辆车");
} else {
console.log("这不是一辆车");
- 自定义类型保护的主要特点是:
- 返回类型谓词,如 vehicle is Car;
- 包含可以准确确定给定变量类型的逻辑语句,如 (vehicle as Car).turnSteeringWheel !== undefined。
交叉类型(&)
这个就和我们运算符里面的 与( && ) 是一样的,既要 … 又要 … 还要 … O(∩_∩)O~
使用 & 作为交叉符号
// 对象接口1
interface One {
name: string
age: number
}
// 对象接口2
interface Two {
gender: string
classRoom: number
}
// 简单准备一个函数
function combine(o1: One, o2: Two): One & Two {
const result = {
...o1,
...o2
}
return result
}
Class(类)
基本方法
在基本方法中有:静态属性,静态方法、成员属性、成员方法、构造器、get set方法,接下来逐个看看:
需要注意的是:在成员属性中,如果不给默认值,并且不使用是会报错的,如果不想报错就给如 ! ,如:name4!:string
class Info {
//静态属性
static name1: string = 'Domesy'
//成员属性,实际上是通过public上进行修饰,只是省略了
nmae2:string = 'Hello' //ok
name3:string //error
name4!:string //ok 不设置默认值的时候必须加入 !
//构造方法
constructor(_name:string){
this.name4 = _name
}
//静态方法
static getName = () => {
return '我是静态方法'
}
//成员方法
getName4 = () => {
return `我是成员方法:${this.name4}`
}
//get 方法
get name5(){
return this.name4
}
//set 方法
set name5(name5){
this.name4 = name5
}
}
const setName = new Info('你好')
console.log(Info.name1) // "Domesy"
console.log(Info.getName()) // "我是静态方法"
console.log(setName.getName4()) // "我是成员方法:你好"
私有字段(#)
在 TS 3.8版本便开始支持ECMACMAScript的私有字段。
需要注意的是私有字段与常规字段不同,主要的区别是:
- 私有字段以 # 字符开头,也叫私有名称;
- 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到。
class Info {
#name: string; //私有字段
getName: string;
constructor(name: string) {
this.#name = name;
this.getName = name
}
setName() {
return `我的名字是${this.#name}`
}
}
let myName = new Info("Domesy");
console.log(myName.setName()) // "我的名字是Domesy"
console.log(myName.getName) // ok "Domesy"
console.log(myName.#name) // error
// Property '#name' is not accessible outside class 'Info'
// because it has a private identifier.(18013)
只读属性(readonly)
只读属性:用 readonly修饰,只能在构造函数中初始化,并且在TS中,只允许将interface、type、class上的属性标识为readonly
- readonly实际上只是在编译阶段进行代码检查
- 被radonly修饰的词只能在 constructor阶段修改,其他时刻不允许修改
class Info {
public readonly name: string; // 只读属性
name1:string
constructor(name: string) {
this.name = name;
this.name1 = name;
}
setName(name:string) {
this.name = name // error
this.name1 = name; // ok
}
}
继承(extends)
继承:是个比较重要的点,指的是子可以继承父的思想,也就是说 子类 通过继承父类后,就拥有了父类的属性和方法,这点与HOC有点类似
这里又个super字段,给不知道的小伙伴说说,其作用是调用父类上的属性和方法
// 父类
class Person {
name: string
age: number
constructor(name: string, age:number){
this.name = name
this.age = age
}
getName(){
console.log(`我的姓名是:${this.name}`)
return this.name
}
setName(name: string){
console.log(`设置姓名为:${name}`)
this.name = name
}
}
// 子类
class Child extends Person {
tel: number
constructor(name: string, age: number, tel:number){
super(name, age)
this.tel = tel
}
getTel(){
console.log(`电话号码是${this.tel}`)
return this.tel
}
}
let res = new Child("Domesy", 7 , 123456)
console.log(res) // Child {."name": "Domesy", "age": 7, "no": 1 }
console.log(res.age) // 7
res.setName('小杜杜') // "设置姓名为:小杜杜"
res.getName() // "我的姓名是:小杜杜"
res.getTel() // "电话号码是123456"
修饰符
主要有三种修饰符:
- public:类中、子类内的任何地方、外部都能调用
- protected:类中、子类内的任何地方都能调用,但外部不能调用
- private:类中可以调用,子类内的任何地方、外部均不可调用
abstract
abstract: 用abstract关键字声明的类叫做抽象类,声明的方法叫做抽象方法
抽象类:指不能被实例化,因为它里面包含一个或多个抽象方法。
抽象方法:是指不包含具体实现的方法;
注:抽象类是不能直接实例化,只能实例化实现了所有抽象方法的子类
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract setAge(age: number) :void;
}
class Child extends Person {
constructor(name: string) {
super(name);
}
setAge(age: number): void {
console.log(`我的名字是${this.name},年龄是${age}`);
}
}
let res = new Person("小杜杜") //error
let res1 = new Child("小杜杜");
res1.setAge(7) // "我的名字是小杜杜,年龄是7"
重写和重载
重写:子类重写继承自父类中的方法
重载:指为同一个函数提供多个类型定义,与上述函数的重载类似
// 重写
class Person{
setName(name: string){
return `我的名字叫${name}`
}
}
class Child extends Person{
setName(name: string){
return `你的名字叫${name}`
}
}
const yourName = new Child()
console.log(yourName.setName('小杜杜')) // "你的名字叫小杜杜"
// 重载
class Person1{
setNameAge(name: string):void;
setNameAge(name: number):void;
setNameAge(name:string | number){
if(typeof name === 'string'){
console.log(`我的名字是${name}`)
}else{
console.log(`我的年龄是${name}`)
}
};
}
const res = new Person1()
res.setNameAge('小杜杜') // "我的名字是小杜杜"
res.setNameAge(7) // "我的年龄是7"
Typescript类中extends和implements的作用
在ES6中,类的继承可以通过extends实现。
在TS的类中,extends同样也是用于类的继承:
class Animal {
name;
sayHello(){}
}
class Dog extends Animal {}
//
const dog = new Dog();
// 在Dog的实例dog中也会存在name属性和sayHello方法,因为Dog继承自Animal类
class Animal {
name: string = '';
sayHello() {
}
}
class Dog extends Animal {}
const dog:Dog = new Dog();
// 和ES6中类似,在Dog的实例dog中也会存在name属性和sayHello方法,因为Dog继承自Animal类
- 在TS类中,类除了可以继承父类还可以继承接口,也叫实现接口,通过关键字implements来实现。
interface Animal {
food: string
eat(food: string): void
}
// Cat类实现Animal接口的时候需要能够兼容Animal接口才行,否则会报错。
class Cat implements Animal {
food: string = 'fish';
eat(food: string): void {
}
}
//
const cat: Cat = new Cat();
TS断言和类型守卫
TS断言
分为三种:类型断言、非空断言、确定赋值断言
当断言失效后,可能使用到:双重断言
类型断言
在特定的环境中,我们会比TS知道这个值具体是什么类型,不需要TS去判断,简单的理解就是,类型断言会告诉编译器,你不用给我进行检查,相信我,他就是这个类型
共有两种方式:
- 尖括号
- as:推荐
//尖括号
let num:any = '小杜杜'
let res1: number = (<string>num).length; // React中会 error
// as 语法
let str: any = 'Domesy';
let res: number = (str as string).length;
但需要注意的是:尖括号语法在React中会报错,原因是与JSX语法会产生冲突,所以只能使用as语法
非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。
我们对比下ES5的代码
我们可以看出来 !可以帮助我们过滤 null和 undefined类型,也就是说,编译器会默认我们只会传来string类型的数据,所以可以赋值为str1
但变成ES5后 !会被移除,所以当传入 null 的时候,还是会打出 null
确定赋值断言
在TS 2.7版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,以告诉TS该属性会被明确赋值。
let num: number;
let num1!: number;
const setNumber = () => num = 7
const setNumber1 = () => num1 = 7
setNumber()
setNumber1()
console.log(num) // error
console.log(num1) // ok
双重断言
断言失效后,可能会用到,但一般情况下不会使用
失效的情况:基础类型不能断言为接口
interface Info{
name: string;
age: number;
}
const name = '小杜杜' as Info; // error, 原因是不能把 string 类型断言为 一个接口
const name1 = '小杜杜' as any as Info; //ok
类型守卫
类型守卫:是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。
我个人的感觉是,类型守卫就是你可以设置多种类型,但我默认你是什么类型的意思
目前,常有的类型守卫共有4种:in关键字、typeof关键字、interfaceof关键字和类型谓词(is)
in关键字
用于判断这个属性是那个里面的
interface Info {
name: string
age: number
}
interface Info1{
name: string
flage: true
}
const setInfo = (data: Info | Info1) => {
if("age" in data){
console.log(`我的名字是:${data.name},年龄是:${data.age}`)
}
if("flage" in data){
console.log(`我的名字是:${data.name},性别是:${data.flage}`)
}
}
setInfo({name: '小杜杜', age: 7}) // "我的名字是:小杜杜,年龄是:7"
setInfo({name: '小杜杜', flage: true}) // "我的名字是:小杜杜,性别是:true"
typeof关键字
用于判断基本类型,如string | number等
const setInfo = (data: number | string | undefined) => {
if(typeof data === "string"){
console.log(`我的名字是:${data}`)
}
if(typeof data === "number"){
console.log(`我的年龄是:${data}`)
}
if(typeof data === "undefined"){
console.log(data)
}
}
setInfo('小杜杜') // "我的名字是:小杜杜"
setInfo(7) // "我的年龄是:7"
setInfo(undefined) // undefined"
interfaceof关键字
用于判断一个实例是不是构造函数,或使用类的时候
class Name {
name: string = '小杜杜'
}
class Age extends Name{
age: number = 7
}
const setInfo = (data: Name) => {
if (data instanceof Age) {
console.log(`我的年龄是${data.age}`);
} else {
console.log(`我的名字是${data.name}`);
}
}
setInfo(new Name()) // "我的名字是小杜杜"
setInfo(new Age()) // "我的年龄是7"
类型谓词(is)
function isNumber(x: any): x is number { //默认传入的是number类型
return typeof x === "number";
}
console.log(isNumber(7)) // true
console.log(isNumber('7')) //false
console.log(isNumber(true)) //false
两者的区别
通过上面的介绍,我们可以发现断言与类型守卫的概念非常相似,都是确定参数的类型,但断言更加霸道,它是直接告诉编辑器,这个参数就是这个类型,而类型守卫更像确定这个参数具体是什么类型。(个人理解,有不对的地方欢迎指出~)
类型别名和接口
类型别名
类型别名:也就是type,用来给一个类型起个新名字
使用类型别名给类型起别名,简化类型的使用。
当同一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用。
// 将一组类型存储到「变量」里, 用 type 来声明这个特殊的「变量」
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
接口(interface)
接口:在面向对象语言中表示行为抽象,也可以用来描述对象的形状。
当一个对象类型被多次使用时,一般会使用接口( interface )来描述对象的类型,达到复用的目的
对象的形状
接口可以用来描述对象,主要可以包括以下数据:可读属性、只读属性、任意属性
- 可读属性:当我们定义一个接口时,我们的属性可能不需要全都要,这是就需要 ? 来解决
- 只读属性:用 readonly修饰的属性为只读属性,意思是指允许定义,不允许之后进行更改
- 任意属性:这个属性极为重要,它是可以用作就算没有定义,也可以使用,比如 [data: string]: any。比如说我们对组件进行封装,而封装的那个组件并没有导出对应的类型,然而又想让他不报错,这时就可以使用任意属性
interface Props {
a: string;
b: number;
c: boolean;
d?: number; // 可选属性
readonly e: string; //只读属性
[f: string]: any //任意属性
}
let res: Props = {
a: '小杜杜',
b: 7,
c: true,
e: 'Domesy',
d: 1, // 有没有d都可以
h: 2 // 任意属性,之前为定义过h
}
let res.e = 'hi' // error, 原因是可读属性不允许更改
函数类型
- 接口能够描述js中对象的外形,除了描述带有属性的普通对象外,也可以描述函数的类型,定义方式如下:
interface Fun{
(a: string, b: string): boolean;
}
- 定义后,可以像使用其他接口一样使用这个函数类型的接口,定义方式如下:
let mySearch: Fun;
mySearch = function(a: string, b: string) {
let result = a.search(b);
if (result == -1) {
return false;
}
else {
return true;
}
}
- 函数中参数的名称可以和接口中定义的名称不一致,如:
let mySearch: Fun;
mySearch = function(c: string, d: string): boolean {
let result = c.search(d);
if (result == -1) {
return false;
}
else {
return true;
}
}
- 函数的参数检查时,会逐个进行,要求对应位置上的类型是兼容的,若不指定类型,ts会自动推断出参数的类型,示例如下:
let mySearch: Fun;
mySearch = function(a, b) {
let result = a.search(b);
if (result == -1) {
return 1;
}
else {
return 2;
}
}
可索引的类型
可以描述能够通过索引得到的类型,如a[10]或a[‘b’],可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型,如下:
interface Test{
[index: number]: string;
}
let myArray: Test;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
- 上述示例中Test具有索引签名,描述了使用number类型去索引Test时会得到string类型的返回值。
只支持字符串和数字两种索引签名,可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。因为使用number来索引时,js会将它转换成string然后再去索引对象,示例如下:
class A{
name: string;
}
class B extends A{
breed: string;
}
// 此处将报错,因为索引必须时number或string的,不能使用其他类型。
interface NotOkay {
[x: number]: A;
[x: string]: B;
}
- 字符串索引签名会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property和obj[“property”]两种形式。下面示例中name的类型与字符串索引类型不匹配,会得到异常提示。
interface A{
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型不是索引类型的子类型
}
- 可以将索引签名设置为只读,这样就防止了给索引赋值:
interface A{
readonly [index: number]: string;
}
let myArray: A= ["Alice", "Bob"];
myArray[2] = "Mallory"; // 此处的索引签名是只读的,所以不能直接设置
索引签名
定义: 索引签名用于描述那些“通过索引得到”的类型
格式: 如[props: string]:any
应用场景: 解决参数问题
export default {}
interface IFullName {
firstName: string
lastName : string
age?: number
singName?: string
[props: string]: any
}
// 注意点: 如果使用接口来限定了变量或者形参, 那么在给变量或者形参赋值的时候,多一个或者少一个都不行
// 实际开发中往往会出现多或者少的情况,怎么解决?
// 少一个或者少多个属性
// 解决方案: 可选属性
let goddass1:IFullName = {firstName: "邱", lastName: "淑贞"};
let goddass2:IFullName = {firstName: "邱", lastName: "淑贞", age: 18};
// 多一个或者多多个属性
// 方案一:使用变量
let info = {firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"};
let goddass3:IFullName = info
// 方案二: 使用类型断言
let goddass4:IFullName = ({firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"}) as IFullName;
// 索引签名?
// 索引签名用于描述那些“通过索引得到”的类型
// 注意点: 对象中的键,会被转化为字符串
interface Ibeauty {
[props: string]: string
}
let name:Ibeauty = {name1: "邱淑贞", name2: "李嘉欣", name3: "周慧敏"};
interface Iage {
[props: string]: number
}
let afe:Iage = {age1: 18, age2: 20};
// 方案三: 索引签名
let goddass5:IFullName = {firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"};
继承
接口继承
接口继承: 可以实现一个接口使用另一个接口的类型约束, 实现接口的复用。
使用关键词extend实现。
如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用。
interface IPerson {
username: string
age: number
gender: string
sayHi: () => void
}
// 接口继承: IStudent 具备 IPerson 的所有约束规则
interface IStudent extends IPerson {
score: number
sleep: () => void
}
const s1: IStudent = {
username: 'james',
age: 19,
gender: '男',
sayHi() {
console.log('詹姆斯')
},
score: 59,
sleep() {
console.log('詹姆斯正在睡觉...')
},
}
- 使用type也能实现类似继承的效果
// 使用 type 实现和 interface 类似继承的效果
type Person = {
username: string
age: number
gender: string
sayHi: () => void
}
// & 与连接符: 既要满足前面的也要满足后面的
// | 或连接符: 满足其中一个即可
type Student = {
score: number
sleep: () => void
} & Person
const s2: Student = {
username: 'james',
age: 28,
gender: '未知',
sayHi() {
console.log('hello!')
},
score: 80,
sleep() {
console.log('我在睡觉')
},
}
函数类型接口
同时,可以定义函数和类,加new修饰的事类,不加new的事函数
interface Props {
(data: number): number
}
const info: Props = (number:number) => number //可定义函数
// 定义函数
class A {
name:string
constructor(name: string){
this.name = name
}
}
interface PropsClass{
new (name: string): A
}
const info1 = (fun: PropsClass, name: string) => new fun(name)
const res = info1(A, "小杜杜")
console.log(res.name) // "小杜杜"
interface(接口)和 type(类型别名)对比
- 相同点:都可以给对象指定类型
- 不同点:
- 接口只能为对象指定类型
- 类型别名不仅可以为对象指定类型,实际上可以为任意类型指定别名
- 推荐:能使用 type 就是用 type
可选参数
使用?给函数指定可选参数类型
// 注意事项: 必选参数不能在可选参数后(可选参数只能出现在参数列表的最后)
const print = (name?: string, gender?: string): void => {
if (name && gender) {
console.log(name, gender)
}
}
联合类型(Union Types)
联合类型(Union Types) : 表示取值可以为多种类型中的一种,未赋值时联合类型上只能访问两个类型共有的属性和方法,如:
const setInfo = (name: string | number) => {}
setInfo('小杜杜')
setInfo(7)
从上面看 setInfo接收一个name,而 name 可以接收 string或number类型,那么这个参数便是联合类型
可辨识联合
可辨识联合:包含三个特点,分别是可辨识、联合类型、类型守卫,
这种类型的本质是:结合联合类型和字面量类型的一种类型保护方法。
如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
也就是上面一起结合使用,这里写个小例子:
interface A {
type: 1,
name: string
}
interface B {
type: 2
age: number
}
interface C {
type: 3,
sex: boolean
}
// const setInfo = (data: A | B | C) => {
// return data.type // ok 原因是 A 、B、C 都有 type属性
// return data.age // error, 原因是没有判断具体是哪个类型,不能确定是A,还是B,或者是C
// }
const setInfo1 = (data: A | B | C) => {
if (data.type === 1 ) {
console.log(`我的名字是${data.name}`);
} else if (data.type === 2 ){
console.log(`我的年龄是${data.age}`);
} else if (data.type === 3 ){
console.log(`我的性别是${data.sex}`);
}
}
setInfo1({type: 1, name: '小杜杜'}) // "我的名字是小杜杜"
setInfo1({type: 2, age: 7}) // "我的年龄是7"
setInfo1({type: 3, sex: true}) // "我的性别是true"
定义了 A、B、C 三次接口,但这三个接口都包含type属性,那么type就是可辨识的属性,而其他属性只跟特性的接口相关。
然后通过可辨识属性type,才能使用其相关的属性
泛型
泛型:Generics,是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
也就是说,泛型是允许同一个函数接受不同类型参数的一种模版,与any相比,使用泛型来创建可服用的组件要更好,因为泛型会保留参数类型(PS:泛型是整个TS的重点,也是难点,请多多注意~)
为什么需要泛型
我们先看看一个例子:
const calcArray = (data:any):any[] => {
let list = []
for(let i = 0; i < 3; i++){
list.push(data)
}
return list
}
console.log(calcArray('d')) // ["d", "d", "d"]
上述的例子我们发现,在calcArray中传任何类型的参数,返回的数组都是any类型
由于我们不知道传入的数据是什么,所以返回的数据也为any的数组
但我们现在想要的效果是:无论我们传什么类型,都能返回对应的类型,针对这种情况怎么办?所以此时泛型就登场了
泛型语法-函数
我们先用泛型对上面的例子进行改造下,
const calcArray = <T,>(data:T):T[] => {
let list:T[] = []
for(let i = 0; i < 3; i++){
list.push(data)
}
return list
}
const res:string[] = calcArray<string>('d') // ok
const res1:number[] = calcArray<number>(7) // ok
type Props = {
name: string,
age: number
}
const res3: Props[] = calcArray<Props>({name: '小杜杜', age: 7}) //ok
经过上面的案例,我们发现传入的字符串、数字、对象,都能返回对应的类型,从而达到我们的目的,接下来我们再看看泛型语法:
function identity <T>(value:T) : T {
return value
}
多类型传参
我们有多个未知的类型占位,我们可以定义任何的字母来表示不同的参数类型
const calcArray = <T,U>(name:T, age:U): {name:T, age:U} => {
const res: {name:T, age:U} = {name, age}
return res
}
const res = calcArray<string, number>('小杜杜', 7)
console.log(res) // {"name": "小杜杜", "age": 7}
泛型接口
定义接口的时候,我们也可以使用泛型
interface A<T> {
data: T
}
const Info: A<string> = {data: '1'}
console.log(Info.data) // "1"
泛型类
同样泛型也可以定义类
class clacArray<T>{
private arr: T[] = [];
add(value: T) {
this.arr.push(value)
}
getValue(): T {
let res = this.arr[0];
console.log(this.arr)
return res;
}
}
const res = new clacArray()
res.add(1)
res.add(2)
res.add(3)
res.getValue() //[1, 2, 3]
console.log(res.getValue) // 1
泛型类型别名
type Info<T> = {
name?: T
age?: T
}
const res:Info<string> = { name: '小杜杜'}
const res1:Info<number> = { age: 7}
泛型默认参数
所谓默认参数,是指定类型,如默认值一样,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
泛型常用字母
用常用的字母来表示一些变量的代表:
- T:代表Type,定义泛型时通常用作第一个类型变量名称
- K:代表Key,表示对象中的键类型;
- V:代表Value,表示对象中的值类型;
- E:代表Element,表示的元素类型;
常用技巧
extends
extends:检验是否拥有其属性 在这里,举个例子,我们知道字符串和数组拥有length属性,但number没有这个属性。
const calcArray = <T,>(data:T): number => {
return data.length // error
}
上述的 calcArray的作用只是获取data的数量,但此时在TS中会报错,这是因为TS不确定传来的属性是否具备length这个属性,毕竟每个属性都不可能完全相同
那么这时该怎么解决呢?
我们已经确定,要拿到传过来数据的 length,也就是说传过来的属性必须具备length这个属性,如果没有,则不让他调用这个方法。
换句话说,calcArray需要具备检验属性的功能,对于上述例子就是检验是否有length的功能,这是我们就需要extends这个属性帮我们去鉴定:
interface Props {
length: number
}
const calcArray = <T extends Props,>(data:T): number => {
return data.length // error
}
calcArray('12') // ok
calcArray([1,3]) //ok
calcArray(2) //error
可以看出calcArray(2)会报错,这是因为number类型并不具备length这个属性
typeof
typeof关键字:我们在类型保护的时候讲解了typeof的作用,除此之外,这个关键字还可以实现推出类型,如下图,可以推断中 Props 包
VUE3+TS语法忽略、eslint忽略
typescript忽略
单行忽略
// @ts-ignore
忽略全文
// @ts-nocheck
取消忽略全文
// @ts-check
eslint忽略+typescript忽略
/* eslint-disable */
const watermark = require("watermark-qyz"); // @ts-ignore
/* eslint-enable */