Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

盘点 TypeScript 中我们常用的那些神奇符号 #129

Open
sisterAn opened this issue Aug 18, 2021 · 0 comments
Open

盘点 TypeScript 中我们常用的那些神奇符号 #129

sisterAn opened this issue Aug 18, 2021 · 0 comments

Comments

@sisterAn
Copy link
Owner

?. 可选链(Optional Chaining)

ES11(ES2020)新增的特性,TypeScript 3.7 支持了这个特性

我们在 为什么要使用 TypeScript? TypeScript 相对于 JavaScript 的优势是什么?中提到 TypeScript 与标准同步发展,并推进了很多 ECMAScripts 语法提案,比如可选链操作符( ?. )、空值合并操作符( ?? )、Throw 表达式、正则匹配索引等,所以,这里介绍的符号大部分都是 ECMAScripts 规范的,TypeScript 特有的只有 ?:!&|

可选链可让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验:

var info = user && user.info

又或是这种

var age = user && user.info && user.info.getAge && user.info.getAge()

很容易命中 Uncaught TypeError: Cannot read property...

用了 Optional Chaining ,上面代码会变成

var info = user?.info
var age = user?.info?.getAge?.()

TypeScript 在尝试访问 user.info 前,会先尝试访问 useruser 既不是 null 也不是 undefined 才会继续往下访问,如果user null 或者 undefined,则表达式直接返回 undefined

即可选链是一种先检查属性是否存在,再尝试访问该属性的运算符 ( ?.

目前,可选链支持以下语法操作:

obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)

?? 空值合并运算符(Nullish coalescing Operator)

ES12(ES2021)新增的特性,TypeScript 3.7 支持了这个特性,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

// {
//   "level": null
// }
var level1 = user.level ?? '暂无等级' // level1 -> '暂无等级'
var level2 = user.other_level ?? '暂无等级' // level1 -> '暂无等级'

与逻辑或操作符(||) 不同,|| 会在左侧操作数为 falsy 值(例如,''0)时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为:

// {
//   "level": 0   
// }
var level1 = user.level || '暂无等级' // level1 -> 暂无等级
var level2 = user.level ?? '暂无等级' // level2 -> 0

?: 可选参数和属性

TypeScript 特有的,在 TypeScript 2.0 支持了这个特性,可选参数和属性会自动把 undefined 添加到他们的类型中,即使他们的类型注解明确不包含 undefined 。例如,下面两个类型是完全相同的:

// 使用--strictNullChecks参数进行编译
type T1 = (x?: number) => string              // x的类型是 number | undefined
type T2 = (x?: number | undefined) => string  // x的类型是 number | undefined

在TypeScript里,我们使用 ?: 最多的情况是在接口中,通常:

interface Point {
  x: number;
  y: number;
}

let point: Point
point = {
  x: 1,
  y: 2
}

其中 point 中的两个属性 xy 都是必须的,如果赋值时缺少任意一个就会报错:

point = {
  x: 1
}
// Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

但接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 所以,这里就需要可选属性( ?. ),即属性是可选的

interface Point {
  x: number;
  y: number;
  z?: number; // 可选属性
}

let point: Point
point = {
  x: 1,
  y: 2
}

在 TypeScript 有两个内置的工具泛型可以帮助我们处理接口的可选操作:

  • Partial :把接口中的所有属性变成可选的
  • Required :将接口中所有可选的属性改为必须的

Partial

Partial 的作用即把类型中的所有属性变成可选的

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
}

例如:

interface Point {
  x: number;
  y: number;
}

type PartialPoint = Partial<Point>

// PartialPoint 相当于:
// type PartialPoint = {
//     x?: number;
//     y?: number;
// }
// 所有属性均可选

它具体是如何实现的喃?

首先了解 keyofkeyof 指的是把我们一个对象里面的键值对里的键( key )一一罗列出来,并把它们联合起来形成一种联合类型:

interface Point {
  x: number;
  y: number;
}

type PointKeys = keyof Point // "x" | "y"

in 是遍历的作用,P in keyof Tkeyof T 进行一个个遍历并且每个都单独拿出来生成新的 "键值对"

所以:

// Partial 语法
// type Partial<T> = {
//   [P in keyof T]?: T[P];
// };

interface Point {
  x: number;
  y: number;
}

type PartialPoint = Partial<Point>

// 第一步↓
type PartialPoint = {
  [P in 'x' | 'y']?: Point[P];
}

// 第二步↓
type PartialPoint = {
  x?: Point["x"];
  y?: Point["y"];
}

// 最终↓
type PartialPoint = {
  x?: number;
  y?: number;
}

因此,实现了 Partial 的效果

Required

Required 的作用刚好与 Partial 相反,就是将接口中所有可选的属性改为必须的,区别就是把 Partial 里面的 ? 替换成了 -?

type Required<T> = {
  [P in keyof T]-?: T[P];
}

例如:

interface Point {
  x?: number;
  y?: number;
}

type RequiredPoint = Required<Point>

// RequiredPoint 相当于:
// type RequiredPoint = {
//     x: number;
//     y: number;
// }
// 所有属性均必须

! 非空断言操作符

TypeScript 特有的,在 TypeScript 2.0 支持了这个特性,在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型的。具体而言,运算 x! 产生一个不包含 nullundefinedx 的值。

function sayHello(hello: string | undefined) {
  const hi1 = hello!.toLowerCase() // OK
  const hi2 = hello.toLowerCase() // Error: Object is possibly 'undefined'
}

仅仅只是骗过了编译器,当你调用 sayHello() 依然会报错,这样使用是因为你已经断言了 hello 一定是 string

let root: (HTMLElement | null) = document.getElementById('root')
// 非空断言操作符--> 这样写只是为了骗过编译器,防止编译的时候报错,但打包后的代码可能还是会报错
root!.style.color = 'red'

非空断言操作符 与 类型守卫

类型守卫用于确保该类型在一定的范围内,常用 typeofinstanceofin

function sayHello(hello: string | undefined) {
    if(typeof hello === 'string') {
        const hi = hello.toLowerCase() 
    }
}

但如果你这样写:

function sayHello(hello: string | undefined) {
  const isSay = typeof hello === 'string'
  if(isSay) {
    const hi1 = hello.toLowerCase() // Error: Object is possibly 'undefined'.
    const hi2 = hello!.toLowerCase() // OK
  }
}

就会报错,即使 isSay 被分配到了类型守卫值,TypeScript 也只会丢失该信息。所以我们一般会 const hi = hello!.toLowerCase() 加上非空断言操作符

但 TypeScript 4.4 RC 会修复这个问题,如果你遇到这个问题,可升级到 TypeScript 4.4 版本后

_ 数字分隔符(Numeric separators)

ES12(ES2021)新增的特性,TypeScript 2.7 就已经支持了这个特性, 这个特性允许用户在数字之间使用下划线_来对数字分组。

const million = 1_000_000
const phone = 173_1777_7777
const bytes = 0xFF_0A_B3_F2
const word = 0b1100_0011_1101_0001

需要注意的是以下函数是不支持分隔符:

  • Number()
  • parseInt()
  • parseFloat()
const million = '1_234_567'

Number(million) 
// NaN

parseInt(million) 
// 1

parseFloat(million)
// 1

** 指数操作符

ES7(ES2016)新增的特性

2**5 // 32

& 交叉类型(Intersection Types)

在 TypeScript 中,交叉类型是将多个类型合并为一个类型,我们可以通过 & 把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

type PointX = {
	x: number;
}

type Point =  PointX & {
	y: number;
}

let point: Point = {
    x: 1,
    y: 2
}

如果多个类型中存在相同的属性喃?

type PointX = {
	x: number;
	z: string;
}

type Point =  PointX & {
	y: number;
    z: number;
}

let point: Point = {
    x: 1,
    y: 2,
    z: 3, // Type 'number' is not assignable to type 'never'.
} 

这里 z 为什么会是 never 类型喃?因为 string & number 的值是永不存在的值,即 never

type PointX = {
	x: number;
	z: {x: string};
}

type Point =  PointX & {
	y: number;
  	z: {z: number};
}

let point: Point = {
    x: 1,
    y: 2,
    z: { 
      x: '1',
      z: 2
    },
}

而这样是可以的,所以,即多个类型合并为一个交叉类型时,如果多个类型间存在同名基础类型属性时,合并后的同名基础类型属性为 never ,如果同名属性均为非基础类型,则可以成功合并

| 联合类型(Union Types)

联合类型表示一个值可以是几种类型之一,用竖线( |)分隔每个类型,所以 number | string | boolean 表示一个值可以是 numberstring,或 boolean

let user: string | number | boolean = 'an'

联合类型通常与 nullundefined 一起使用:

const helloName = (name: string | undefined) => {
  /* ... */
};

你也可以这么用:

type Hello = 'say' | 'kiss' | 'smile';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant