TypeScript学习笔记(二) 类型系统详解

大家好,我是半虹,这篇文章来讲 TypeScript 中的类型系统


1、简介

所谓类型系统,就是编程语言中用于管理类型的一系列规则和约束

现代编程语言采用的类型系统有很多,大体上可以分为两种:

  1. 隐式推导:无需显式声明类型,在运行时自动进行推导,如 JavaScript 和 Python 等动态语言
  2. 显式声明:要求显式声明类型,在编译时即可进行检查,如 C 和  Java  等静态语言

TypeScript 则是处于上述二者之间,可以显式声明部分类型,然后编译时推导其余部分

需要注意,TypeScript 属于弱类型、静态类型语言,二者划分标准如下:

  1. 强弱类型按照是否允许隐式类型转换划分

    如果允许隐式类型转换,则是弱类型 ✓

    如果禁止隐式类型转换,则是强类型

  2. 动静类型按照类型检查执行时机进行划分

    如果在编译时进行检查,是静态类型 ✓

    如果在运行时进行检查,是动态类型


类型系统中的类型其实是值的分类,定义了该值存储的信息类型,以及可以对其执行的操作

类型检查所做的事,就是通过使用的类型和对应的用法判断执行的操作是否有效


需要注意的是,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

下面就来逐一进行介绍

TypeScript 中新增的类型

(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 类型的变量不能当作任意类型的值赋予其它变量,除了 anyunknown
  • 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  外的任何值,包括 anyunknown

事实上 这个概念还挺抽象,什么是不存在的类型,不存在的类型有啥用


先解答第一个问题:什么是不存在的类型,存在两种情况会出现 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 在日常开发中挺少用到的,定义这个类型主要是为了保证类型系统的完整性

我们可以从集合论的 角度来重新理解这个类型:

上面介绍的 anyunknown 可以是任何类型,因此可以看作是其它类型的全集,称为顶层类型

而  never  则恰恰相反,不包括任何类型,因此可以看作是空集,称为底层类型


回顾下  never  的性质:该类型无法赋予除 never  外的任何值,包括 anyunknown

从集合论的角度去解释:空集不包括除空集外的集合,也就是说 never 不包含其它类型

由此也可以推出新性质:该类型可以赋值给任何类型的变量

从集合论的角度去解释:空集是所有集合的子集,因此所有类型都包含 never


(4)void

void 表示没有类型,或者可以将其视为空值,是 nullundefined 的联合类型

void 和  never 区别如下:

  • 从定义上说,never 表示不存在 的类型,void 表示没有类型
  • 从赋值上说,never 只能赋值为 nevervoid 可以赋值为 voidnullundefined
  • 对函数来说,
    • 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 中原有的类型

在 JavaScript 中,将值分为八种类型,这些类型在 TypeScript 中依然沿用, 并被称为基本类型

基本类型里面又可分为两类,分别是:原始类型引用类型

  • 原始类型:numberstringbooleanbigintsymbolnullundefined
  • 引用类型:object

为了方便理解和记忆,下面分成三组来进行介绍:

  • 第一组是原始类型中那些特定的类型,包括:numberstringbooleanbigintsymbol
  • 第二组是原始类型中那些特殊的类型,包括:nullundefined
  • 第三组是引用类型,引用类型中就只是包括:object

(5)numberstringbooleanbigintsymbol

上述五个类型都有特定的含义和取值:

类型含义取值
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

总结上面介绍的四种类型声明方式如下:

  1. 让 TypeScript 推导出值的类型,如 let x = true;
  2. 让 TypeScript 推导出具体的值,如 const x = true;
  3. 明确告诉 TypeScript 值的类型,如 let x:boolean = true;
  4. 明确告诉 TypeScript 具体的值,如 let x:true = true;

(6)nullundefined

这里两个类型具有特殊的含义和取值:

类型含义取值
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  有三种取值:numberstringsymbol,此外:

  • 如果同时存在具体属性以及索引签名,那么二者不能冲突
  • 如果同时声明多个索引签名,那么这些索引签名不能冲突
// 编译报错
// 具体属性以及索引签名冲突
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|... 表示只要一个类型属于 AB...,那么它就属于联合类型 A|B|...

换句话说也即只要符合 AB... 类型中的一个,能赋值给联合类型 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&... 表示只有类型同时属于 AB...,那么它才属于交叉类型 A&B&...

换句话说也即只有符合 AB... 类型中的所有,能赋值给交叉类型 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)类型别名

为了类型复用,就像使用变量声明(letconst)为值声明别名一样

我们同样可以为类型声明别名,即  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',
};

一般来说,我们会使用 letconst 声明值,使用 typeinterface 声明类型

enumclass 则比较特殊 , 二者声明的既是类型也是值



好啦,本文到此结束,感谢您的阅读!

如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议

如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值