常用类型
any
any
的含义是任意类型, 一旦将变量限制为any
, 那就意味着放弃了对该变量的类型限制
注意
any
类型的变量可以赋值给任意类型的变量
// 显式的any: 明确声明 a 是 any
let a: any
a = 100
a = 'Halo'
a = false
// 隐式的any: 没有明确的表示 b 的类型是 any, TS 主动推断出的 any
let b
b = 100
b = 'Halo'
b = false
// 注意: any 类型的变量可以赋值给任意类型的变量
let x: string
x = a // 无警告
console.log(x) // false
unknown
unknown
的含义是未知类型
unknown
可以理解为一个类型安全的any
, 适用于不确定数据的具体类型
// 设置 a 的类型为 unknown
let a: unknown
// 以下对 a 赋值均正常
a = 100
a = 'Halo'
a = false
// 设置 x 的数据类型为 string
let x: string
x = a // 警告: 不能将类型“unknown”分配给类型“string”。
// 第一种方法: 判断类型
if (typeof a === 'string') {
x = a
}
// 第二种方法: 类型断言
x = a as string
x = <string>a
unknown
会强制开发者在使用之前进行类型检查, 从而提供更强的类型安全性
let strA: string
strA = 'Halo'
strA.toUpperCase() // 无警告
let strB: any
strB = 'Halo'
strB.toUpperCase() // 无警告
let strC: unknown
strC = 'Halo'
strC.toUpperCase() // 警告: “strC”的类型为“未知”
// 使用类型断言强制指定 strC 的类型为 string
(strC as string).toUpperCase() // 无警告
never
never
的含义是任何值都不是, 简而言之就是不能有值,undefined
,null
,''
,false
,0
都不行
- 几乎不用
never
去直接限制变量, 因为没有意义
// 指定 a 的类型为 never, 那就意味着 a 以后不能赋值任何数据
let a: never
// 以下对 a 的所有赋值都会有警告
a = 1
a = false
a = ''
a = undefined
a = null
never
一般是TS
主动推断出来的
// 指定 x 的类型为 string
let x: string
x = 'Halo'
if (typeof x === 'string') {
console.log(x.toUpperCase())
} else {
console.log(x) // TS 会推断出此处的 x 是 never, 因为没有任何一个值符合此处的逻辑
}
never
可用于限制函数的返回值
// 限制 throwError 函数不需要用任何返回值, 任何值都不行
function throwError(str: string): never {
throw new Error(`程序异常退出: ${str}`)
}
void
void
通常用于函数返回值, 含义是表示函数不返回任何值, 调用者也不应依赖其返回值进行任何操作
注意
如果函数中没有return
去指定函数的返回值, 那么函数是没有显式返回值的, 但会有一个隐式返回值, 就是undefined
虽然
logMessage
函数的返回类型为void
, 但也可以接受undefined
的即:
undefined
是void
可以接受的一种空值
function logMessage(msg: string): void {
console.log(msg)
}
logMessage('Halo')
- 以下写法均符合规范
// 无警告
function logMessageA(msg: string): void {
console.log(msg)
}
// 无警告
function logMessageB(msg: string): void {
console.log(msg)
return
}
// 无警告
function logMessageC(msg: string): void {
console.log(msg)
return undefined
}
- 限制函数返回值
void
和undefined
的区别
function demoA(): void {
console.log('@')
}
let resA = demoA()
if (resA) { // 警告: 无法测试 "void" 类型的表达式的真实性
}
function demoB(): undefined {
console.log('@')
}
let resB = demoB()
if (resB) { // 无警告
}
void 和 undefined
void
是一个广泛的概念, 用来表达空值, 而undefined
则是这种空值的具体实现之一, 因此可以说undefined
是void
能接受的空状态的一种具体形式void
包含undefined
, 但void
表达的语义超越了单纯的undefined
, 它是一种意图上的约定, 而不仅仅是特定值的限制
void 总结
若函数返回类型为
void
- 从语义上讲, 函数是可以返回
undefined
的, 至于显式返回还是隐式返回无所谓 - 函数调用者不应关心函数的返回值, 也不应依赖返回值进行任何操作, 即使返回了
undefined
object
关于小写
object
和大写Object
, 实际开发中使用相对较少, 因为范围太大了
小写 object
object
的含义是: 所有非原始类型, 可存储对象, 函数, 数组等, 由于限制的范围比较宽泛, 在实际开发中使用相对较少
// a 能存储的类型是 非原始类型
let a: object;
// 以下代码将 非原始类型 赋值给 a, 所以均符合要求
a = {};
a = { name: "张三" };
a = [1, 2, 3];
a = function () {};
a = new String("123");
class Person {}
a = new Person();
// 以下代码将 原始类型 赋值给 a, 有警告
a = 1; // 不能将类型“number”分配给类型“object”
a = true; // 不能将类型“boolean”分配给类型“object”
a = "Halo"; // 不能将类型“string”分配给类型“object”
a = null; // 不能将类型“null”分配给类型“object”
a = undefined; // 不能将类型“undefined”分配给类型“object”
大写 Object
- 官方描述: 所有可以调用
Object
方法的类型 - 简单记忆: 除了
undefined
和null
的任何值 - 由于限制的范围太大了, 所以实际开发中使用频率较低
// b 能存储的类型是可以调用到 Object 方法的类型
let b: Object
// 以下代码将 Object 的实例对象赋值给 a, 均无警告
b = {};
b = { name: "张三" };
b = [1, 2, 3];
b = function () {};
b = new String("123");
class PersonB {}
b = new PersonB();
b = 1;
b = true;
b = "Halo";
// null 和 undefined 不是 Object 的实例对象, 会有警告
b = null // 不能将类型“null”分配给类型“Object”
b = undefined // 不能将类型“undefined”分配给类型“Object”
声明对象类型
- 实际开发中限制一般对象, 通常使用以下形式
// 声明对象类型 person 必须有 name 和 age 属性, gender 为可选属性
let personA: { name: string, age: number, gender?: string }
personA = { name: '张三', age: 18 }
// 含义同上, 可以用 ; 分号分隔
let personB: { name: string; age: number; gender?: string }
// 含义同上, 可以用换行分隔
let personC: {
name: string
age: number
gender?: string
}
- 索引签名: 允许定义对象可以具有任意数量的属性, 这些属性的键和类型是可变的, 常用于描述类型不确定的属性, 具有动态属性的对象
// 声明对象类型 person 必须有 name 和 age 属性, gender 为可选属性, 同时可以有任意数量, 任意类型
let personD: {
name: string
age: number
gender?: string
[key: string]: any // 索引签名, 可以将 key 替换为其他单词
}
personD = {
name: '李四',
age: 20,
gender: '男',
score: 60,
email: '123@xx.com'
}
声明函数类型
TS
中的=>
在函数类型声明时表示函数类型, 描述其参数类型和返回类型JS
中的=>
是一种定义函数的语法, 是具体的函数实现- 函数类型声明还可以使用接口, 自定义类型等方式
let count: (a: number, b: number) => number
count = function(x, y) {
return x + y
}
声明数组类型
let arrA: string[]
arrA = ['a', 'b', 'c']
let arrB: Array<number> // <number> 泛型指定类型
arrB = [1, 2, 3]
tuple
元组
tuple
是一种特殊的数组类型, 可以存储固定数量的元素, 并且每个元素的类型是已知的且可以不同 元组用于精确描述一组值的类型,?
表示可选元素
// 第一个元素必须是 string 类型, 第二个元素必须是 number 类型
let arrA: [string, number]
// 第一个元素必须是 number 类型, 第二个元素是可选的, 如果有则必须是 boolean 类型
let arrB: [number, boolean?]
// 第一个元素必须是 number 类型, 后面的元素可以是任意数量的 string 类型
let arrC: [number, ...string[]]
// 赋值
arrA = ['123', 321]
arrB = [100]
arrB = [200, false]
arrC = [10, '1', '2', '3']
arrC = [20]
// 不可以赋值, arrA 声明是两个元素, 赋值是三个
arrA = ['Halo', 123, false] // 不能将类型“[string, number, boolean]”分配给类型“[string, number]”
enum
枚举
enum
可以定义一组命名常量, 枚举可以增强代码的可读性, 让代码更好维护
/**
* 根据调用 walk 函数时传入的不同参数,执行不同的逻辑,存在的问题是调用 walk 传参时没有任何提示,开发者很容易写错字符串内容
* 并且用于判断逻辑的 up down left right 是连续且相关的一组值,此时就特别适合使用枚举
*/
function walk(str: string) {
switch (str) {
case "up":
console.log("向上");
break;
case "down":
console.log("向下");
break;
case "left":
console.log("向左");
break;
case "right":
console.log("向右");
break;
default:
console.log('未知方向');
break;
}
}
walk('up')
walk('down')
walk('left')
walk('right')
数字枚举
数字枚举是最常见的枚举类型, 其成员的值会自动递增, 且数字枚举还具备反向映射的特点, 可以通过值来获取对应的枚举成员名称
// 定义枚举
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
}
// 打印信息
console.log(Direction)
/*
{
0: 'UP',
1: 'DOWN',
2: 'LEFT',
3: 'RIGHT',
'UP': 0,
'DOWN': 1,
'LEFT': 2,
'RIGHT': 3
}
*/
// 反向映射
console.log(Direction.UP) // 0
console.log(Direction[0]) // UP
// 无法为“UP”赋值,因为它是只读属性
Direction.UP = 'UPUP'
- 可以指定枚举成员的初始值, 其后的成员值会自动递增
enum Direction {
UP = 6,
DOWN,
LEFT,
RIGHT
}
console.log(Direction.UP) // 6
console.log(Direction.DOWN) // 7
- 使用枚举优化上例
walk
函数, 代码更加直观易读, 而且类型安全, 同时也更易于维护
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
}
function walk(str: Direction) {
switch (str) {
case Direction.UP:
console.log("向上");
break;
case Direction.DOWN:
console.log("向下");
break;
case Direction.LEFT:
console.log("向左");
break;
case Direction.RIGHT:
console.log("向右");
break;
default:
console.log('未知方向');
break;
}
}
字符串枚举
枚举成员的值是字符串
注意
字符串枚举会丢失反向映射
// 字符串枚举
enum DirectionString {
UP = 'UP',
DOWN = 'DOWN',
LEFT = 'LEFT',
RIGHT = 'RIGHT'
}
let dir: DirectionString = DirectionString.UP
console.log(dir) // UP
常量枚举
官方描述: 常量枚举是一种特殊枚举类型, 它使用
const
关键字定义, 在编译时会被内联, 避免生成一些额外的代码何为编译时内联?
所谓内联就是
TS
在编译时, 会将枚举成员引用替换为它们的实际值, 而不是生成额外的枚举对象, 这样可以减少生成的JS
代码量, 并且提高运行时的性能
// 常量枚举 编译为 js 时会少很多代码
const enum DirectionConst {
UP,
DOWN,
LEFT,
RIGHT
}
console.log(DirectionConst.UP)
type
type
可以为任意类型创建别名, 让代码更简洁, 可读性更强, 同时能更方便的进行类型复用和扩展
type 基本用法
类型别名使用
type
关键字定义,type
后跟类型名称, 如下代码中num
是类型别名
type num = number
let age: num
age = 18
type 联合类型
联合类型是一种高级类型, 表示一个值可以是几种不同类型之一, 类似逻辑或
// 联合类型
type Status = number | string
let a: Status = 100
let b: Status = '200'
type Gender = '男' | '女'
let x: Gender = '男'
let y: Gender = '女'
type 交叉类型
交叉类型
Intersection Types
允许将多个类型合并为一个类型, 合并后的类型将拥有所有被合并类型的成员, 交叉类型通常用于对象类型, 类似逻辑且
// 交叉类型
// 面积
type Area = {
width: number; // 宽
height: number; // 高
}
// 地址
type Address = {
num: number // 楼号
cell: number // 单元号
room: string // 房间号
}
type House = Area & Address
const house: House = {
width: 30,
height: 3,
num: 1,
cell: 2,
room: '3'
}
一种特殊情况
先观察如下两段代码
- 代码段1(正常): 在函数定义时, 限制函数返回值为
void
, 那么函数的返回值就必须是空
function demo(): void {
// 返回 undefined 合法
return undefined
// 以下返回均不合法
return 100
return false
return null
return []
}
- 代码段2(特殊): 使用类型声明限制函数返回值为
void
时,TS
并不会严格要求函数返回空
type LogFunc = () => void
const f1: LogFunc = () => {
return 100 // 允许返回非空值
}
const f2: LogFunc = () => 200 // 允许返回非空值
const f3: LogFunc = function() {
return 300 // 允许返回非空值
}
let res = f1()
if (res) { // 警告: 无法测试 "void" 类型的表达式的真实性
}
为什么会这样?
是为了确保如下代码成立,
Array.prototype.push
方法的返回是一个数字, 而Array.prototype.forEach
方法期望其回调的返回类型是void
为了兼容
JS
箭头函数简写时会默认return
的语法格式
const src = [1,2,3]
const dst = [0]
src.forEach(el => dst.push(el))
类相关知识
类
class Person {
// 属性声明
name: string
age: number
// 构造器
constructor(name: string, age: number){
this.name = name
this.age = age
}
// 方法
speak() {
console.log(`我叫${this.name}, 今年${this.age}岁`)
}
}
const man = new Person('张三', 18)
console.log(man)
man.speak()
类的继承
class Student extends Person {
grade: string
constructor(name: string, age: number, grade: string){
// 调用父类构造器
super(name, age)
this.grade = grade
}
// 方法
study() {
console.log(`${this.name}正在努力学习中......`)
}
// 重写父类方法
override speak() {
console.log(`我是学生, 我叫${this.name}, 今年${this.age}岁, 在读${this.grade}年级`)
}
}
const stu = new Student('李四', 12, '初三')
console.log(stu)
stu.speak()
stu.study()
属性修饰符
修饰符 | 含义 | 具体规则 |
---|---|---|
public | 公开的 | 类内部, 子类, 类外部可以访问 |
protected | 受保护的 | 类内部, 子类可以访问 |
private | 私有的 | 类内部可以访问 |
readonly | 只读属性 | 属性无法修改 |
public 修饰符
class Person {
// 属性声明
public name: string
public age: number
constructor(name: string, age: number){
this.name = name
this.age = age
}
public speak() {
// 类内部访问
console.log(`我叫${this.name}, 今年${this.age}岁`)
}
}
class Student extends Person {
study() {
// 子类访问
console.log(`${this.name}正在努力学习中......`)
}
}
const man = new Person('张三', 18)
// 类外部访问
console.log(man.name)
属性的简写形式
注意
简写必须要写修饰符
class Person {
/* 简写前
public name: string
public age: number
constructor(name: string, age: number){
this.name = name
this.age = age
}
*/
// 简写后
constructor(public name: string, public age: number) {
}
}
protected 修饰符
class Person {
constructor(
protected name: string,
protected age: number
) {}
protected getDetail() {
// 类内部可以访问
return `我叫${this.name}, 今年${this.age}岁`
}
introduce() {
console.log(this.getDetail())
}
}
const man = new Person('Tom', 18)
// 类外部无法访问
man.name
man.age
man.getDetail()
class Student extends Person {
study() {
this.introduce()
// 子类可以访问
console.log(`${this.name}正在学习...`)
}
}
private 修饰符
class Person {
constructor(
public name: string,
public age: number,
private IDCard: string
) {}
getInfo() {
return `我叫${this.name}, 今年${this.age}岁`
}
private getPrivateInfo() {
// 类内部可以访问
return `身份证号码: ${this.IDCard}`
}
getFullInfo() {
return `${this.getInfo()}, ${this.getPrivateInfo()}`
}
}
const man = new Person('Tom', 18, '110114567544323454')
// 类外部无法访问
man.IDCard
man.getPrivateInfo()
class Student extends Person {
study() {
// 子类无法访问
console.log(this.IDCard)
this.getPrivateInfo()
}
}
readonly 修饰符
class Person {
constructor(
public name: string,
readonly age: number
) {}
}
const man = new Person('张三', 18)
man.name = '李四'
man.age = 20 // 无法为“age”赋值,因为它是只读属性
抽象类
概述: 抽象类是一种无法被实例化的类, 专门用来定义类的结构和行为, 类中可以写抽象方法, 也可以写具体实现, 抽象类主要用来为其派生类提供一个基础结构, 要求其派生类必须实现其中的抽象方法
简记: 抽象类不能实例化, 其意义是可以被继承, 抽象类里可以用普通方法, 也可以有抽象方法
通过以下场景理解抽象类
- 定义一个抽象类
Package
, 表示所有包裹的基本结构, 任何包裹都有重量属性weight
, 包裹都需要计算运费, 但是不同类型的包裹(标准, 特快)有不同的运费计算方式, 因此用于计算运费的caleculate
方法是一个抽象方法, 必须由具体的子类来实现
// 声明抽象类
abstract class Package {
// 构造器
constructor(public weight: number) {}
// 声明抽象方法
abstract calculate(): number
// 具体方法
printPackage() {
console.log(`包裹重量为: ${this.weight}kg, 运费为: ${this.calculate()}元`)
}
}
- 标准
StandardPackage
类继承了Package
, 实现caleculate
方法
// 标准
class StandardPackage extends Package {
constructor(
weight: number,
public unitPrice: number
) {
super(weight)
}
calculate(): number {
return this.weight * this.unitPrice
}
}
const sp = new StandardPackage(10, 5)
sp.printPackage()
- 特快
ExpressPackage
类继承了Package
, 实现caleculate
方法
// 特快
class ExpressPackage extends Package {
constructor(
weight: number,
public unitPrice: number,
public additional: number,
) {
super(weight)
}
calculate(): number {
if (this.weight > 10) {
return 10 * this.unitPrice + (this.weight - 10) * this.additional
}
return this.weight * this.unitPrice
}
}
const ep = new ExpressPackage(16, 10, 2)
ep.printPackage()
抽象类总结
何时使用抽象类?
- 定义通用接口: 为一组相关的类定义通用的行为, 如方法或属性
- 提供基础实现: 在抽象类中提供某些方法或为其提供基础实现, 这样派生类就可以继承这些实现
- 确保关键实现: 强制派生类实现一些关键行为
- 共享代码和逻辑: 当多个类需要共享部分代码时, 抽象类可以避免代码重复
接口
接口
interface
是一种定义结构的方式, 主要作用是为类, 对象, 函数等规定一种契约, 可以确保代码的一致性和安全性, 但要注意interface
只能定义格式, 不能包含任何实现
定义类结构
// PersonInterface 接口
interface PersonInterface {
name: string
age: number
speak(n: number): string
}
class Person implements PersonInterface {
constructor(
public name: string,
public age: number
) {}
speak(n: number): string {
console.log(n)
return `我叫${this.name}, 今年${this.age}岁`
}
}
const man = new Person('Tom', 18)
man.speak(3)
定义对象结构
// UserInterface 接口
interface UserInterface {
name: string
age: number
gender?: string
readonly IDCard: string
run: (n: number) => void
}
const user: UserInterface = {
name: 'Tom',
age: 18,
gender: '男',
IDCard: '110123341111',
run(n) {
for (let i = 0; i < n; i++) {
console.log(`跑`)
}
}
}
定义函数结构
// CountInterface 接口
interface CountInterface {
(a: number, b: number): number
}
const count: CountInterface = (x, y) => {
return x + y
}
count(3, 9)
接口之间的继承
// PersonInterface 接口
interface PersonInterface {
name: string,
age: number
}
// StudentInterface 接口继承 PersonInterface 接口
interface StudentInterface extends PersonInterface {
grade: string
}
const stu: StudentInterface = {
name: '张三',
age: 18,
grade: '3'
}
接口自动合并(可重复定义)
// PersonInterface 接口
interface PersonInterface {
name: string,
age: number
}
// 自动跟上面的合并
interface PersonInterface {
gender: string
}
const p: PersonInterface = {
name: '李四',
age: 18,
gender: '男'
}
接口总结
何时使用接口?
- 定义对象的格式: 描述数据模型,
API
响应格式, 配置对象等, 是实际开发中使用较多的场景 - 类的契约: 规定一个类需要实现哪些属性和方法
- 自动合并: 一般用于扩展第三方库的类型, 这种特性在大型项目中可能会用到
一些相似概念的区别
interface 和 type 的区别
- 相同点:
interface
和type
都可以用于定义对象结构, 两者在许多场景中是可以互换的 - 不同点:
interface
更专注于定义对象和类的结构, 支持继承, 合并type
可以定义类型别名, 联合类型, 交叉类型, 但不支持继承和合并
interface 和抽象类的区别
- 相同点: 都用于定义一个类的格式, 应该遵循的契约
- 不同点:
- 接口: 只能描述结构, 不能有任何实现代码, 一个类可以实现多个接口
- 抽象类: 既可以包含抽象方法, 也可以包含具体方法, 一个类只能继承一个抽象类