3.11 字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
3.11.1 基本使用
- 思考以下代码,两个变量的类型分别是什么?
let str1 = 'Hello TS'
const str2 = 'Hello TS'
- 通过 TS 类型推断机制,可以得到答案:
- 变量 str1 的类型为:string
- 变量 str2 的类型为:'Hello TS'
- 解释:
- str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string
- str2 是一个常量(const),它的值不能变化只能是 'Hello TS',所以,它的类型为:'Hello TS'
- 注意:此处的 'Hello TS',就是一个字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:
{
let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;
}
比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型,如下具体示例:
{
let specifiedStr: 'this is string' = 'this is string';
let str: string = 'any string';
specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
str = specifiedStr; // ok
}
'this is string' 字面量类型可以给 string 类型赋值,但是 string 类型不能给 'this is string' 字面量类型赋值。
3.11.2 字符串字面量类型
一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下代码所示:
let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'
实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型(后面会讲解),用来描述拥有明确成员的实用的集合。
如下代码所示,我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。
type Direction = 'up' | 'down';
function move(dir: Direction) {
// ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'
通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。
因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。
3.11.3 数字字面量类型及布尔字面量类型
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
在上述代码中,我们限定了 size 属性为字符串字面量类型 'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。
3.11.4 let和const分析
我们先来看一个 const 示例,如下代码所示:
{
const str = 'this is string'; // str: 'this is string'
const num = 1; // num: 1
const bool = true; // bool: true
}
在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。
接下来我们看看如下所示的 let 示例:
{
let str = 'this is string'; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean
}
在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。
这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):
str = 'any string';
num = 2;
bool = false;
我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型,下面我们着重介绍一下。
3.11.5 使用模式和场景
- 使用模式:字面量类型配合联合类型一起使用
- 使用场景:用来表示一组明确的可选值列表
- 比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上、下、左、右中的任意一个
// 使用自定义类型:
type Direction = 'up' | 'down' | 'left' | 'right'
function changeDirection(direction: Direction) {
console.log(direction)
}
// 调用函数时,会有类型提示:
changeDirection('up')
- 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个
- 优势:相比于 string 类型,使用字面量类型更加精确、严谨
3.11.6 类型拓宽(Type Widening)
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:
let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
因为第 1~2 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。
因为第 3 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。
第 4~5 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。
基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。
{
const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}
实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似 "Type Widening" (类型拓宽)的设计,下面我们具体来了解一下。
比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:
{
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
/** -----分界线------- */
const z = null; // 类型是 null
/** -----分界线------- */
let anyFun = (param = null) => param; // 形参类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 undefined
}
注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”。
3.11.7 类型缩小(Type Narrowing)
在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。
比如,我们可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型,具体示例如下:
{
let func = (anything: any) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
}
在 VS Code 中 hover 到第 4 行的 anything 变量提示类型是 string,到第 6 行则提示类型是 number。
同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:
{
let func = (anything: string | number) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
};
}
当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:
{
type Goods = 'pen' | 'pencil' |'ruler';
const getPenCost = (item: 'pen') => 2;
const getPencilCost = (item: 'pencil') => 4;
const getRulerCost = (item: 'ruler') => 6;
const getCost = (item: Goods) => {
if (item === 'pen') {
return getPenCost(item); // item => 'pen'
} else if (item === 'pencil') {
return getPencilCost(item); // item => 'pencil'
} else {
return getRulerCost(item); // item => 'ruler'
}
}
}
在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if
语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。
那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost
呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为“pencil”。
事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:
const getCost = (item: Goods) => {
if (item === 'pen') {
item; // item => 'pen'
} else {
item; // => 'pencil' | 'ruler'
}
}
3.12 枚举类型
3.12.1 基本使用
- 枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值
- 枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个
// 创建枚举
enum 枚举名 {
枚举项 = 枚举值,
枚举项 = 枚举值,
}
- 解释:
- 使用
enum
关键字定义枚举 - 约定枚举名称以大写字母开头
- 枚举中的多个值之间通过
,
(逗号)分隔 - 定义好枚举后,直接使用枚举名称作为类型注解
- 使用
3.12.2 数字枚举
- 问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
- 解释:通过将鼠标移入 Gender.Boy,可以看到枚举成员 Boy 的值为 1
- 注意:枚举成员是有值的,默认为:从 0 开始自增的数值
- 我们把,枚举成员的值为数字的枚举,称为:
数字枚举
- 当然,也可以给枚举中的成员初始化值
{
/*
✅开发需求:
用户编辑界面,用户选择的性别 男 女 未知
后端接口参数要求传的是数字对应为 1 0 -1
🎉开发经验:
TS项目中可通过设计枚举结构,提高源代码的可读性
*/
// 定义枚举结构
// enum Gender {
// Boy = 1,
// Girl = 0,
// Unknown = -1
// }
// 🔔补充:枚举项会自动自增,默认从 0 开始
enum Gender {
Unknown = -1,
Girl,
Boy,
}
// 模拟后端所需的参数
const query: Gender = {
name: '用户名',
// gender: Gender.Boy 等价于 gender: 1,🔔但代码可读性更高
gender: Gender.Boy,
}
console.log('query', query)
3.12.3 字符串枚举-了解即可
- 字符串枚举:枚举成员的值是字符串
- 注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
3.12.4 枚举实现原理
- 枚举是 TS 为数不多的非 JavaScript 类型级扩展(不仅仅是类型)的特性之一
- 因为:其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
- 也就是说,其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码
enum Gender {
Unknown = -1,
Girl = 0,
Boy = 1,
}
// 会被编译为以下 JS 代码:
var Gender;
(function (Gender) {
Gender[Gender["Unknown"] = -1] = "Unknown";
Gender[Gender["Girl"] = 0] = "Girl";
Gender[Gender["Boy"] = 1] = "Boy";
})(Gender || (Gender = {}));
- 说明:枚举与前面讲到的字面量类型+联合类型组合的功能类似,都用来表示一组明确的可选值列表
- 一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效
3.13 any 类型
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型.
- 原则:不推荐使用 any!这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)
- 一个变量设置类型为any后相当于对该变量关闭了TS的类型检测
- 当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示
let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}
let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.
- 解释:以上操作都不会有任何类型错误提示,即使可能存在错误
- 尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型
- 其他隐式具有 any 类型的情况
声明变量不提供类型也不提供默认值
let something; something = 'seven'; something = 7; something.setName('Tom');
等价于
let something: any; something = 'seven'; something = 7; something.setName('Tom');
函数参数不加类型
- 注意:因为不推荐使用 any,所以,这两种情况下都应该提供类型
在许多场景下,这太宽松了。使用 any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any
类型,就无法使用 TypeScript 提供的大量的保护机制。请记住,any 是魔鬼!
尽量不要用any。
为了解决 any
带来的问题,TypeScript 3.0 引入了 unknown
类型。
3.14 unknown 类型
3.14.1 基本使用
unknown
与any
一样,所有类型都可以分配给unknown
:
// unknown 表示未知类型的值
let e: unknown;
e = 10;
e = 'hello';
unknown
与any
的最大区别是: 任何类型的值可以赋值给any
,同时any
类型的值也可以赋值给任何类型。unknown
任何类型的值都可以赋值给它,但它只能赋值给unknown
和any
。看看下面这个例子:
let notSure: unknown = 4;
let uncertain: any = notSure; // OK
let notSure: any = 4;
let uncertain: unknown = notSure; // OK
let notSure: unknown = 4;
let uncertain: number = notSure; // Error
unknown 实际上就是一个类型安全的any
unknown类型的变量,不能直接赋值给其他变量(除了自己和any)
如果不缩小类型,就无法对unknown
类型执行任何操作:
function getDog() {
return '123'
}
const dog: unknown = {hello: getDog};
dog.hello(); // Error
这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof
、类型断言
等方式来缩小未知范围:
function getDogName() {
let x: unknown;
return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
const upName = dogName.toLowerCase(); // OK
}
但是这样才能赋值太麻烦了,所以引出了类型断言。
3.14.2 类型断言
有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型。
类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。
不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as 语法做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
语法
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
以上两种方式虽然没有任何区别,但是尖括号格式会与react中JSX产生语法冲突,因此我们更推荐使用 as 语法。
好了,对于上面unknown 类型
赋值的类型也能解决了。
function getDogName() {
let x: unknown;
return x;
};
const dogName = getDogName();
// 类型断言
let upName = (dogName as string).toLowerCase(); // OK
let upName = (<string>dogName).toLowerCase(); // OK
告诉解析器我就是这个类型的就可以了。
3.14.3 非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。
具体看以下示例:
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
3.14.4 确定赋值断言
允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
通过 let x!: number;
确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
3.15 总结
类型声明
类型声明是TS非常重要的一个特点
通过类型声明可以指定TS中变量(参数、形参)的类型
指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值
语法:
let 变量: 类型; let 变量: 类型 = 值; function fn(参数: 类型, 参数: 类型): 类型{ ... }
自动类型判断
- TS拥有自动的类型判断机制
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
- 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明
类型:
类型 例子 描述 number 1, -33, 2.5 任意数字 string 'hi', "hi", hi 任意字符串 boolean true、false 布尔值true或false 字面量 其本身 限制变量的值就是该字面量的值 any * 任意类型 unknown * 类型安全的any void 空值(undefined) 没有值(或undefined) never 没有值 不能是任何值 object 任意的JS对象 array [1,2,3] 任意JS数组 tuple [4,5] 元素,TS新增类型,固定长度数组 enum enum 枚举,TS中新增类型