大家好,我是半虹,这篇文章来讲 TypeScript 中的类型系统
1、简介
所谓类型系统,就是编程语言中用于管理类型的一系列规则和约束
现代编程语言采用的类型系统有很多,大体上可以分为两种:
- 隐式推导:无需显式声明类型,在运行时自动进行推导,如 JavaScript 和 Python 等动态语言
- 显式声明:要求显式声明类型,在编译时即可进行检查,如 C 和 Java 等静态语言
TypeScript 则是处于上述二者之间,可以显式声明部分类型,然后编译时推导其余部分
需要注意,TypeScript 属于弱类型、静态类型语言,二者划分标准如下:
-
强弱类型按照是否允许隐式类型转换划分
如果允许隐式类型转换,则是弱类型 ✓
如果禁止隐式类型转换,则是强类型
-
动静类型按照类型检查执行时机进行划分
如果在编译时进行检查,是静态类型 ✓
如果在运行时进行检查,是动态类型
类型系统中的类型其实是值的分类,定义了该值存储的信息类型,以及可以对其执行的操作
类型检查所做的事,就是通过使用的类型和对应的用法判断执行的操作是否有效
需要注意的是,TypeScript 的代码可以分为值代码以及类型代码
let x:number = 1;
就以上述为例,对于变量 x
,类型声明为 number
,且取值为 1
变量的类型对于变量的取值及其可执行的操作具有一定的约束性
上述代码以显式声明的方式指定变量类型,如果没有显式声明,就会自动做隐式推导
let x = 1;
这行代码会通过变量的初始赋值隐式推导出变量的类型为数字
因此只需在必要时显式声明,而多数情况下可以让其隐式推导,这样才能最大地发挥出优势
但是不能盲目依赖隐式推导,无论是显式声明还是说隐式推导,都需要遵循相关规则
下面我们就来详细介绍 TS 中的各种类型以及相关的使用规则
2、详解
TypeScript 中的类型有很多,为了方便理解和记忆,可以将其分为两组
第一组是 TypeScript
中新增的类型:
any
unknown
never
void
第二组是 JavaScript
中原有的类型:
number
string
boolean
bigint
symbol
undefined
null
object
下面就来逐一进行介绍
(1)any
any
可以表示任何类型,如果指定变量的类型为 any
,那么就像在 JavaScript 中使用变量一样
这些 变量没有任何限制,可以对其进行任何操作 ,如:
any
类型的变量可以赋予任意类型的值,作为等号左值存在any
类型的变量可以当作任意类型的值,作为等号右值存在any
类型的变量还能调用属性以及方法,也能作为函数调用
正是由于 any
可以是各种类型 , 因此各种类型的操作都被允许
设置类型 any
就是告诉编译器 , 关闭对该变量的类型检查操作
let x:any;
let y:number;
let z:string;
// 可以赋予任意类型的值,作为等号左值存在
x = '0';
x = 123;
// 可以当作任意类型的值,作为等号右值存在
y = x;
z = x;
// 这时还会污染其它变量,导致其它变量出错
// 举个例子:
z.substring(0);
// 这行代码编译一切正常,但是运行时会报错
// 原因在于:
// 变量 z 声明的类型是 string,然而实际的取值是 number,因为 x 赋值给 z 时不会进行类型检查
// 所以编译时 z 看作是 string,调用 substring 方法没有问题
// 但是运行时 z 实际是 number,没有 substring 方法就会报错
x.a; // 调用不存在的属性,编译时正常,运行时报错
x.b(); // 调用不存在的方法,编译时正常,运行时报错
x(); // 数值作为函数调用,编译时正常,运行时报错
上述操作都能通过类型检查,但是却在运行时报错
这种行为是非常危险的,相当于类型检查完全失效,导致错误在运行阶段才暴露出来
正是因为如此,我们应尽量避免使用 any
,否则就失去了使用 TS 的意义
最后提醒大家一下,除了显式声明类型为 any
,那些无法推导出类型的变量也视作 any
例如我们定义变量时没有显式声明类型 ,也没有初始赋值,那么该变量就会推导为 any
// 情况一
let x; // 推导为 any 类型
x = '0'; // 赋值后不改变类型,仍是 any 类型
x = 123; // 赋值后不改变类型,仍是 any 类型
// 情况二
function add(a, b) { // 参数 a 和 b 推导为 any 类型,不再进行类型检查
return a + b
}
因此,对那些无法判断类型的特殊情况 ,需要做显式声明,从而避免被隐式推导为 any
(2)unknown
unknown
用于表示未知类型,这意味着它可能是各种类型,可以看作是类型安全的 any
就和 any
类似, unknown
类型的变量可以赋予任意类型的值
let x:unknown;
// 可以赋予任意类型的值
x = '0'; // 编译正常
x = 123; // 编译正常
但与 any
不同, unknown
类型的变量在使用上有严格的限制
unknown
类型的变量不能当作任意类型的值赋予其它变量,除了any
和unknown
unknown
类型的变量不能调用属性以及方法,不能作为函数调用
let x:unknown;
let y:number;
let z:string;
let a:any;
let b:unknown;
// 可以赋予任意类型的值
x = '0';
// 不能当作任意类型的值赋予其他变量,除了 any 和 unknown
// 因此可以防止污染其它变量【重要】
y = x; // 编译报错
z = x; // 编译报错
a = x; // 编译正常
b = x; // 编译正常
// 不能调用属性以及方法,也不能作为函数调用
x.length; // 编译报错,即使 x 是字符串
x.substr(); // 编译报错,即使 x 是字符串
x(); // 编译报错
这些使用上的限制在一定程度上保证了类型安全,但这样好像就无法使用 unknown
类型的变量了
怎么才能使用 unknown
呢?答案就是类型收敛,使用前先证明 unknown
具体是哪种或哪些类型
let x:unknown;
x = '0';
if (typeof x === 'string') {
x.length; // 编译正常
x.substr(); // 编译正常
}
由于 unknown
基本可以替代 any
且更安全,所以凡是需要 any
的场景,请优先考虑 unknown
另外 unknown
类型并不会被 隐式 推导出来,因此想要使用 unknown
,就要显式声明
(3)never
never
表示不存在的类型,并且该类型无法赋予除 never
外的任何值,包括 any
和 unknown
事实上 这个概念还挺抽象,什么是不存在的类型,不存在的类型有啥用
先解答第一个问题:什么是不存在的类型,存在两种情况会出现 never
- 不可能返回值的函数表达式,返回值的类型就是
never
- 不可能进入到的条件分支中,其中类型会被判为
never
// 不可能返回值的函数表达式
// 第一种情况:总是抛出异常
let fn1 = function():never { // 返回类型为 never
throw new Error()
}
// 不可能返回值的函数表达式
// 第二种情况:存在有死循环
let fn2 = function():never { // 返回类型为 never
while (true) {}
}
// 不可能进入到的条件分支中
function fn(x: number|string) { // 参数 x 要么是 number 要么是 string
switch (typeof x) {
case 'number': // 此时 x 是 number
x;
break;
case 'string': // 此时 x 是 string
x;
break;
default: // 此时 x 是 never(不可能进入)
x;
}
}
再回答第二个问题:不存在的类型有啥用,这里对应上述的情况来介绍
- 在函数表达式,声明返回类型
never
可以检查函数是否具有终点 - 在条件分支中,兜底分支设置
never
可以检查分支判断是否完全
// 在函数表达式,声明返回类型 never
let run = function():never {
// ...
// 假设该函数是主进程,需要一直运行,永远不会返回
}
// 在条件分支中,兜底分支设置 never
enum X {
A,
B,
}
function handleX(x:X) {
switch (x) {
case X.A:
// ...
break;
case X.B:
// ...
break;
default:
// 这里可以定义一个 never 类型的变量并赋予 x,因为 never 只能赋予 never,所以只要有可能进入这一分支,编译器就会报错
// 当在枚举类型 X 新增加了值,但是处理函数 handleX 忘记处理时,这一分支就有可能成立,使得编译器报错从而提醒开发者处理
const exhaustiveCheck:never = x;
}
}
一般情况下,never
在日常开发中挺少用到的,定义这个类型主要是为了保证类型系统的完整性
我们可以从集合论的 角度来重新理解这个类型:
上面介绍的 any
和 unknown
可以是任何类型,因此可以看作是其它类型的全集,称为顶层类型
而 never
则恰恰相反,不包括任何类型,因此可以看作是空集,称为底层类型
回顾下 never
的性质:该类型无法赋予除 never
外的任何值,包括 any
和 unknown
从集合论的角度去解释:空集不包括除空集外的集合,也就是说 never
不包含其它类型
由此也可以推出新性质:该类型可以赋值给任何类型的变量
从集合论的角度去解释:空集是所有集合的子集,因此所有类型都包含 never
(4)void
void
表示没有类型,或者可以将其视为空值,是 null
和 undefined
的联合类型
void
和 never
区别如下:
- 从定义上说,
never
表示不存在 的类型,void
表示没有类型 - 从赋值上说,
never
只能赋值为never
,void
可以赋值为void
、null
、undefined
- 对函数来说,
never
表示不可能返回值的函数表达式的返回类型(注意粗体-
void
表示没显式返回值的函数表达式或函数声明的返回类型
// 没显式返回值的函数表达式(普通函数)
// 返回类型为 void
let fn1 = function() {
console.log();
}
// 没显式返回值的函数表达式(箭头函数)
// 返回类型为 void
let fn2 = () => {
console.log();
}
// 没显式返回值的函数声明
// 返回类型为 void
function fn3() {
console.log();
}
// 不可能返回值的函数表达式(普通函数)
// 返回类型为 never
let fn4 = function() {
throw new Error();
}
// 不可能返回值的函数表达式(箭头函数)
// 返回类型为 never
let fn5 = () => {
throw new Error();
}
// 不可能返回值的函数声明
// 返回类型为 void【注意!!!】
function fn6() {
throw new Error();
}
never
类型为啥需要强调是函数表达式呢?具体原因可参考 Stack Overflow 上的解答
在 JavaScript 中,将值分为八种类型,这些类型在 TypeScript 中依然沿用, 并被称为基本类型
基本类型里面又可分为两类,分别是:原始类型和引用类型
- 原始类型:
number
、string
、boolean
、bigint
、symbol
、null
、undefined
- 引用类型:
object
为了方便理解和记忆,下面分成三组来进行介绍:
- 第一组是原始类型中那些特定的类型,包括:
number
、string
、boolean
、bigint
、symbol
- 第二组是原始类型中那些特殊的类型,包括:
null
、undefined
- 第三组是引用类型,引用类型中就只是包括:
object
(5)number
、string
、boolean
、bigint
、symbol
上述五个类型都有特定的含义和取值:
类型 | 含义 | 取值 |
---|---|---|
number | 数字 | 整数、浮点数、二进制数、八进制数、十六进制数、… |
string | 字符串 | 普通字符串、模版字符串 |
boolean | 布尔 | true / false |
bigint | 大整数 | 通过 BigInt 函数创建,或在整数字面量后加 n |
symbol | 标识符 | 通过 Symbol 函数创建 |
下面举例说明:
// 数字
let n1:number = 7; // 整数
let n2:number = 3.14; // 浮点数
let n3:number = 0b1111; // 二进制数字
let n4:number = 0o7777; // 八进制数字
let n5:number = 0xffff; // 十六进制数字
let n6:number = NaN; // 非法数字
let n7:number = Infinity; // 无限
// 字符串
let s1:string = 'Jeffy'; // 普通字符串
let s2:string = `my name is ${s1}` // 模版字符串
// 布尔
let b1:boolean = true; // 真
let b2:boolean = false; // 假
// 大整数
let bi1:bigint = BigInt(123);
let bi2:bigint = 123n;
// 标识符
let sb1:symbol = Symbol();
let sb2:symbol = Symbol();
上述这些例子,即使省去显式类型声明,那么也能根据初始赋值正确推导类型
无论哪种情况,确定变量类型后也可以在该类型的取值范围之内修改变量的值
但是有些时候,我们希望变量只有唯一取值,这时也有提供对应的类型,称为类型字面量
所谓类型字面量就是表示只有一个值的类型
// 数字
let n1:1234 = 1234; // 使用类型字面量声明变量类型为 1234
// 字符串
let s1:'00' = '00'; // 使用类型字面量声明变量类型为 '00'
// 布尔
let b1:true = true; // 使用类型字面量声明变量类型为 true
// 大整数
let bi:123n = 123n; // 使用类型字面量声明变量类型为 123n
// 标识符
const sb:unique symbol = Symbol(); // 必须使用 const 声明,并且类型字面量为:unique symbol
除了显式声明,还有一种情况可以隐式推导出类型字面量,那就是使用 const
声明变量
由于 const
所定义的变量赋值后不可修改,因此编译器会推导出最窄的类型
// 数字
const n1 = 1234; // 推导的类型为字面量:1234
// 字符串
const s1 = '00'; // 推导的类型为字面量:'00'
// 布尔
const b1 = true; // 推导的类型为字面量:true
// 大整数
const bi = 123n; // 推导的类型为字面量:123n
// 标识符
const sb = Symbol(); // 推导的类型为字面量:unique symbol
总结上面介绍的四种类型声明方式如下:
- 让 TypeScript 推导出值的类型,如
let x = true;
- 让 TypeScript 推导出具体的值,如
const x = true;
- 明确告诉 TypeScript 值的类型,如
let x:boolean = true;
- 明确告诉 TypeScript 具体的值,如
let x:true = true;
(6)null
、undefined
这里两个类型具有特殊的含义和取值:
类型 | 含义 | 取值 |
---|---|---|
null | 缺少值 | null |
undefined | 未定义 | undefined |
还是举例说明:
// 显式声明
let a1:null;
let b1:undefined;
// 隐式推导
let a2 = null;
let b2 = undefined;
在 JavaScript 中,通常没有严格区分二者,但是它们的语义确实有着细微的差别
(7)object
object
是非常重要的一种类型,用于表示对象的结构,在日常开发中十分常见
对象采用的是结构化类型,就是只关心对象有哪些属性,不关心对象叫什么名称
按照上文的类型声明方法,我们可以显式声明变量类型为 object
let obj:object = {
a: 1,
b: 2,
}
// 看似没有问题对吧
// 但是当你访问对象的属性时,就会发现编译错误
obj.a; // error TS2339: Property 'a' does not exist on type 'object'.
obj.b; // error TS2339: Property 'b' does not exist on type 'object'.
即使已经声明类型为 object
,但却无法访问定义的属性,这很奇怪啊,啥也做不了
其实这是因为类型为 object
,只能表示这是对象,但是其对于对象的属性全然不知
那么我们不显式声明,然后让其进行隐式推导可以吗
let obj = {
a: 1,
b: 2,
}
// 编译正常
obj.a = 3;
obj.b = 4;
// 对象能够正常访问以及修改属性
const tst = {
a: 1,
b: 2,
}
// 编译正常
tst.a = 3;
tst.b = 4;
// 即使使用 const 声明变量,也不像 number/string/boolean/bigint/symbol 会导致属性推导类型缩小为单个值
上述例子就可以证明,隐式推导确实是使用对象类型的一种方式
但是如果有一些属性是复杂类型,那么这种方式不一定足够智能
其实更好的声明方法是使用对象字面量句法,在花括号里面逐一声明每个属性的类型
每个属性的名称和类型使用冒号分隔,属性之间使用逗号或分号分隔
// 变量定义,同时声明类型
let obj:{
a: number,
b: number,
};
// 变量赋值,需要满足类型
obj = {
a: 1,
b: 2,
};
// 为了清晰,上述变量定义和变量赋值分两步进行
需要注意下,对象类型的变量赋值时,必须严格满足声明的类型,不少、不多、不错
另外一点是,对象类型的变量赋值后,读写不存在的类型会报错,删除已存在的类型也会报错
// 赋值时,少了必要的属性,编译报错
obj = {
a: 1,
}
// 赋值时,多了额外的属性,编译报错
obj = {
a: 1,
b: 2,
c: 3,
}
// 读写不存在的类型会报错
obj.d;
obj.d = 4;
// 删除已存在的类型会报错
delete obj.a;
上面介绍如何声明对象中属性的类型
这里再说如何声明对象中方法的类型,方法类型用函数类型描述
let obj:{
a: number;
b: number;
fn1: (x:number, y:number) => number;
// 或者
// fn1 (x:number, y:number): number;
} = {
a: 1,
b: 2,
fn1: (x, y) => {
return x + y;
}
}
上面提到过,对象类型的变量赋值时,必须严格满足声明的类型,不少、不多、不错
但是有时候,实际赋值的属性可能比预先声明的属性少,这时就要用可选属性来声明
可选属性说明该属性在赋值时可以被忽略,其实也相当于允许被赋值为:undefined
可选属性声明时只需在属性名后加上问号就可以
let user: {
firstname: string; // 正常属性
lastname?: string; // 可选属性,等价于 `lastname: string|undefined;`
};
// 赋值时缺少可选属性,编译正常
user = {
firstname: 'Eren',
}
// 赋值时带有可选属性且正常赋值,编译正常
user = {
firstname: 'Eren',
lastname: 'Jaeger',
}
// 赋值时带有可选属性且赋值为 undefined,编译正常
user = {
firstname: 'Eren',
lastname: undefined,
}
// 由于 user.lastname 是 string 或 undefined,所以使用时需要做类型收敛
相反的情况,实际赋值的属性可能比预先声明的属性多,这时就要用索引签名来声明
特别是对象的属性很多,或者是无法知道所有的属性时,就需要使用
语法为 [name: T]: U
,表示:类型为 T
的键对应的值是 U
类型
其中的 name
可任意指定,键的类型 T
有三种取值:number
、string
、symbol
,此外:
- 如果同时存在具体属性以及索引签名,那么二者不能冲突
- 如果同时声明多个索引签名,那么这些索引签名不能冲突
// 编译报错
// 具体属性以及索引签名冲突
let obj1: {
a: number;
b: number;
[property: string]: string; // 类型为 string 的键对应的值是 string 类型
}
// 编译报错
// 多个索引签名冲突
let obj2: {
[property: string]: string; // 类型为 string 的键对应的值是 string 类型
[property: number]: number; // 类型为 number 的键对应的值是 number 类型
}
// 这个例子看似没有问题,但在 JavaScript 内部,所有数字属性名都会转成字符串属性名
// 因此数字索引签名也要满足字符串索引签名的要求,只有数字索引签名的值也改成 string 类型才不会报错
// 编译通过
let obj3: {
a: number;
b: number;
[property: string]: number;
}
obj3 = {
a: 1,
b: 2,
test1: 3, // 额外属性,编译通过,满足索引签名
test2: 4, // 额外属性,编译通过,满足索引签名
}
再来介绍下只读属性,只读属性赋予初始值之后,就再也无法进行修改
只需在属性名前加上 readonly
修饰符即可,类似于使用 const
声明
// 如果只读属性是一个原始类型
let obj1: {
readonly name: string;
};
// 初始赋值
obj1 = {
name: 'Jeffy'
};
// 修改属性
obj1.name = 'Tom'; // 编译报错
obj1 = { // 编译正常
name: 'Tom'
};
// 如果只读属性是一个引用类型
let obj2: {
readonly name: {
firstname: string;
lastname: string;
}
};
// 初始赋值
obj2 = {
name: {
firstname: 'Eren',
lastname: 'Jaeger',
}
};
// 修改属性
obj2.name.firstname = 'Mikasa'; // 编译正常
obj2.name = { // 编译报错
firstname: 'Mikasa',
lastname: 'Akkāman',
}
// 整体来说,与 const 声明表现类似
在介绍对象类型的开头,我们有提到对象采用的是结构化类型,就是说:
- 若对象
A
和对象B
有相同结构 ,则对象A
和对象B
是相同类型 - 若对象
A
能满足对象B
的结构 ,则对象A
就兼容对象B
的类型
在这两种情况下,凡是可以使用对象 A
的地方都可以使用对象 B
代替【重要】
这个现象被称为:结构类型原则,即不关心对象是否严格相似,只关心程序是否可以运行
let A: {
a: number;
b: number;
} = {
a: 1,
b: 2,
};
let B: {
a: number;
} = {
a: 1,
};
// 这里对象 A 兼容对象 B,因为对象 B 只是要求具有类型为数字的属性 a,对象 A 满足这个要求
// 按照结构类型原则,我们可以将对象 A 赋值给对象 B,事实上确实可以,编译器没有报错
B = A; // 编译正常
// 但是上面我们提到过,同时也用例子证明过,对象类型的变量赋值时,必须严格满足声明的类型,不能多出额外的属性
// 这里为什么就可以呢?二者的区别为:这里使用的是变量赋值,上面使用的是字面量赋值
// 使用字面量赋值时,会触发严格字面量检查,要求字面量必须满足声明的类型,因此报错【重要】
B = { // 编译报错
a: 1,
b: 2,
}
如果定义的类型中所有属性均为可选,那么这种类型被人们称为弱类型
按结构类型原则,这些属性都是可选的对象类型可以赋值为任意的对象
但是为了强化对弱类型的检查,编译器认为至少需要一个属性存在重叠
let C: {
a?: number;
b?: number;
};
let D = {
c: 3
}
let E = {
a: 1,
c: 3,
}
C = D; // 编译报错
C = E; // 编译正常
3、拓展
至此,TypeScript 中的类型系统以及其中常用的十二种类型已经全部介绍完毕
最后 来补充一些没有提到但却比较重要的类型和操作
(1)联合类型
联合类型又称并集类型,使用 |
表示,语义上是:或 / or
联合类型 A|B|...
表示只要一个类型属于 A
或 B
或 ...
,那么它就属于联合类型 A|B|...
换句话说也即只要符合 A
或 B
或 ...
类型中的一个,都能赋值给联合类型 A|B|...
// 类型声明:number|string
// 取值范围:只要符合 number 或 string 类型中的一个
let x:number|string;
x = 123; // 编译正常
x = '0'; // 编译正常
x = true; // 编译报错
由于联合类型可能是多种类型,因此一般只允许访问这些类型中的共有成员
若想访问联合类型中某个类型的特有成员,则需要先证明目前就是目标类型,也就是类型收敛
function handle1(x: number|string) {
x.toString(); // 编译正常
x.toFixed(2); // 编译报错
x.split('0'); // 编译报错
}
function handle2(x: number|string) {
switch (typeof x) {
case 'number':
x.toFixed(2); // 编译正常,此时类型收敛为 number,因此可以访问 number 的特有成员
break;
case 'string':
x.split('0'); // 编译正常,此时类型收敛为 string,因此可以访问 string 的特有成员
break;
}
}
组成联合类型的子类型可以是类型字面量,这种情况也是十分常见且常用的
let x:'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday';
// 为了方便阅读,可以分行书写,并在第一个子类型之前加上 |
let y:
| 'Monday'
| 'Tuesday'
| 'Wednesday'
| 'Thursday'
| 'Friday';
(2)交叉类型
交叉类型又称交集类型,使用 &
表示,语义上是:和 / and
交叉类型 A&B&...
表示只有类型同时属于 A
和 B
和 ...
,那么它才属于交叉类型 A&B&...
换句话说也即只有符合 A
和 B
和 ...
类型中的所有,才能赋值给交叉类型 A&B&...
// 类型声明:number&string
// 取值范围:只有符合 number 和 string 类型中的所有
let x:number&string;
// 但是这种情况实际上并不存在
// 一种类型不可能既是数字也是字符串,因此 TypeScript 会将其推断为 never
交叉类型通常用于合并对象类型, 或者增加新的属性
let obj1:
{ val1: number, val2: number } &
{ val1: string, val3: string };
// 实际上相当于:
// let obj1: {
// val1: number & string;
// val2: number;
// val3: string;
// }
(3)枚举类型
枚举类型通常用于定义一组带名字的常量,从而使得代码更加清晰可读
可以通过 enum
关键字声明,并且为每一个成员指定一个数字或字符串作为其值
enum ColorNumber {
RED = 0,
GREEN = 1,
BLUE = 2,
}
enum ColorString {
RED = 'RED',
GREEN = 'GREEN',
BLUE = 'BLUE',
}
如果没有显式指定值,那么默认就是从零开始、加一递增
如果间隔指定若干值且值为数字,那么每个指定的值开始加一递增,此时可能会存在重复的值
如果只是指定一个值且值为字符串,则这个指定的值必须位于最后,其余从零开始、加一递增
enum E1 {
A, // 0 (从零开始)
B, // 1 (加一递增,0 + 1 = 1)
C, // 2 (加一递增,1 + 1 = 2)
}
enum E2 {
A, // 0 (从零开始)
B, // 1 (加一递增,0 + 1 = 1)
C = 4,
D, // 5 (加一递增,4 + 1 = 5)
E, // 6 (加一递增,5 + 1 = 6)
F = 3,
G = 2,
H, // 3 (加一递增,2 + 1 = 3)
}
enum E3 {
A, // 0 (从零开始)
B, // 1 (加一递增,0 + 1 = 1)
C = 'TEST',
}
访问枚举属性时,就像是访问对象属性一样,可以使用点或方括号
枚举属性的值既可以写成枚举类型,也可以写成对应的数字或字符串类型
enum E0 {
A,
B,
C,
}
// 既可以写成枚举类型
let v1:E0 = E0.A; // 既可以使用点访问
// 也可以写成数字类型
let v2:number = E0.A; // 又可以使用方括号访问
最后提醒一下哈,枚举属性都是只读的,无法为其重新赋值
enum E0 {
A,
B,
C,
}
E0.A = 1; // error TS2540: Cannot assign to 'A' because it is a read-only property.
(4)类型别名
为了类型复用,就像使用变量声明(let
、const
)为值声明别名一样
我们同样可以为类型声明别名,即 type
;另外也可以将对象类型提炼成接口,即 interface
类型别名和接口都能用于定义类型,下面来简单地介绍下最基础的用法
// 类型别名,注意这里是有等号的,就像是为值声明别名
// 可以用于:任意类型
type TestType = {
a: number;
b: number;
};
// 这是接口,注意这里是没等号的,语法上存在一些区别
// 只能声明:对象类型
interface TestInterface {
a: number;
b: number;
};
定义类型后,就可以将某个值变量声明为该类型
// 使用类型别名
let test1:TestType; // 变量指定类型
user1 = { // 变量指定值
a: 1,
b: 2,
}
// 这里使用接口
let test2:TestInterface; // 变量指定类型
user2 = { // 变量指定值
a: 1,
b: 2,
}
上篇文章我们说过,TypeScript 中的类型与值是分离的
因此我们甚至可以,定义相同名称的类型和值,本质上是因为类型和值存在于不同的命名空间中
// 类型代码
type user = { // 声明类型的别名是 user
name: string;
}
// 值代码
let user:user = { // 声明值的别名也是 user
name: 'Jeffy',
}
而在编译时,类型相关的代码都会被删除,包括有类型声明和类型运算
例如上述代码编译后如下:
var user = {
name: 'Jeffy',
};
一般来说,我们会使用 let
和 const
声明值,使用 type
和 interface
声明类型
而 enum
和 class
则比较特殊 , 二者声明的既是类型也是值
好啦,本文到此结束,感谢您的阅读!
如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)