# ! 非空断言
!
后缀表达式可以用于断言操作对象是非 null
和非 undefined
类型。即 x!
,将从 x
值域中排除 null
和 undefined
。
# 忽略 undefined 和 null 类型
代码语言:javascript复制function myFunc(maybeString: string | undefined | null) {
// Type 'string | undefined | null' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
# 调用函数时忽略 undefined 类型
代码语言:javascript复制type NumGenerator = () => number;
function myFunc(generator: NumGenerator | undefined) {
// Object is possibly 'undefined'.
const result = generator(); // Error
const result = generator!(); // Ok
}
!
非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用过程中,需要注意。
# 确定赋值断言
允许在实例属性或变量声明后面放置一个 !
,从而告诉 TypeScript 该属性会被明确地赋值。
let x: number;
initialize();
// Variable 'x' is used before being assigned.
console.log(2 * x); // Error
function initialize() {
x = 10;
}
使用确定赋值断言:
代码语言:javascript复制let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
# ?. 可选链
可选链,可以在遇到 null
或 undefined
立即停止某些表达式的运行。
obj?.prop
obj?.[expr]
arr?.[index]
func?.(...args)
可选属性示例:
代码语言:javascript复制const val = a?.b;
// "use strict";
// const val = a === null || a === void 0 ? void 0 : a.b;
?.
与 &&
运算符行为略有不同,&&
专门用于检测 falsy
值,比如空字符串、0
、false
、null
等,而 ?.
专门用于检测 null
和 undefined
。对于 0
和 空字符串,并不会出现短路。
# 可选元素访问
可选元素的访问允许访问非标识符的属性,如任意字符串、数字索引或 Symbol。
代码语言:javascript复制function tryGetArrayElement<T>(arr?: T[], index: number = 0) {
return arr?.[index];
}
// "use strict";
// function tryGetArrayElement(arr, index = 0) {
// return arr === null || arr === void 0 ? void 0 : arr[index];
// }
# 可选链与函数调用
函数调用时,如果被调用的方法不存在,使用可选链可以使表达式自动返回 undefined
而不是抛出一个异常。
let result = obj.customMethod?.();
// "use strict";
// var _a;
// let result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);
- 如果存在一个属性名且该属性名对应的值不是函数类型,使用
?.
仍然会产生一个 TypeError 异常。 - 可选链的运算行为被局限在属性的访问、调用以及元素的访问——不会延伸到后续的表达式中。
# ?? 空值合并
??
,当左侧操作数为 null
或 undefined
时,其返回右侧的操作数,否则返回左侧的操作数。
与 ||
的区别,逻辑或会在左侧为 falsy
值是返回右侧的操作数。如果使用 ||
为某些变量设置默认值时,会遇到意料之外的事,如 falsy
值(空字符串、NaN
或 0
)时。
const foo = null ?? 'default string'
console.log(foo); // default string
const baz = 0 ?? 2022;
console.log(baz); // 0
// "use strict";
// const foo = null !== null && null !== void 0 ? null : 'default string';
// console.log(foo);
// const baz = 0 !== null && 0 !== void 0 ? 0 : 2022;
// console.log(baz);
# 短路
当空值合并运算符的左表达式不为 null
或 undefined
时,不会对右表达式求值。
function A() {
console.log('A');
return undefined;
}
function B() {
console.log('B');
return false;
}
function C() {
console.log('C');
return "foo";
}
console.log(A() ?? C());
// A
// C
// foo
console.log(B() ?? C());
// B
// false
# 不能与 && 或 || 操作符共用
空值合并运算符 ??
不能与 &&
或 ||
操作符共用,否则会产生语法错误。
null || undefined ?? "foo"; // SyntaxError
true && undefined ?? "foo"; // SyntaxError
(null || undefined) ?? "foo"; // "foo" (works)
# 与可选链操作符的关系
空值合并运算符 ??
和 可选链操作符 ?.
都是针对 undefined
和 null
。
interface Customer {
name: string;
city?: string;
}
let customer: Customer = {
name: "Cell"
};
let customerCity = customer?.city ?? "Unknown";
console.log(customerCity); // Unknown
# ?: 可选属性
接口除了可以用于对类的一部分行为进行抽象外,也常用于对「对象的形状(Shape)」进行描述。
代码语言:javascript复制interface Person {
name: string;
age: number;
}
let cell: Person = {
name: "Cell",
age: 18
};
可选属性
代码语言:javascript复制interface Person {
name: string;
age?: number;
}
let cell: Person = {
name: "Cell"
};
# 工具类型-Partial<T>
通过 Partial<T>
工具类型,可以将所有属性设置为可选的。
interface PullDownRefreshConfig {
threshold: number;
stop: number;
}
type PullDownRefreshOpts = Partial<PullDownRefreshConfig>;
/**
* type PullDownRefreshOpts = {
* threshold?: number | undefined;
* stop?: number | undefined;
* }
*/
实现原理:
代码语言:javascript复制type Partial<T> = {
[P in keyof T]?: T[P];
};
# 工具类型-Required<T>
通过 Require<T>
工具类型,可以将所有可选属性设置为必选的。
interface PullDownRefreshConfig {
threshold: number;
stop: number;
}
type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>
实现原理:
代码语言:javascript复制type Required<T> = {
[P in keyof T]-?: T[P];
};
通过 -?
移除了可选属性中的 ?
,使得属性从可选变为必选。
# & 合并
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 &
运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type PartialPointX = { x: number };
type Point = PartialPointX & { y: number };
let point: Point = {
x: 1,
y: 2
};
# 同名基础类型属性的合并
代码语言:javascript复制interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string;
}
type XY = X & Y; // (c: string & number) , d: string , e: string
type YX = Y & X; // (c: number & string) , e: string , d: string
c
的类型会变成 never
,因为 string
和 number
之间没有交集。
# 同名非基础类型属性的合并
代码语言:javascript复制interface D { d: boolean }
interface E { e: string }
interface F { f: number }
interface A { x: D }
interface B { x: E }
interface C { x: F }
type ABC = A & B & C; // (x: D & E & F)
let abc: ABC = {
x: {
d: true,
e: "Cell",
f: 2022
}
};
在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么可以成功合并。
# | 分隔
在 TypeScript 中联合类型表示取值可以为多种类型中的一种,联合类型使用 |
分隔每个类型。联合类型通常与 null
或 undefined
一起使用,表示一个值可以为 null
或 undefined
或某个类型。
const sayHello = (name: string | undefined) => {
if (name === undefined) {
console.log("Hello, Stranger");
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
};
- 字面量类型
// 用来约束取值只能是某几个值中的一个
let num: 1 | 2 = 1;
type EventNames = "click" | "scroll" | "mousemove";
使用联合类型时,必须尽量把当前值的类型收窄为当前值的实际类型,而类型保护就是实现类型收窄的一种手段。
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。即,类型保护可以确保一个字符串是一个字符串,尽管它的值可以是一个数字。
类型保护与特性检测并不是完全不同,其主要思路是尝试检测属性、方法、原型,来确定如何处理值。
# 类型保护-in
代码语言:javascript复制interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log(`Name: ${emp.name}`);
if ("privileges" in emp) {
console.log(`Privileges: ${emp.privileges}`);
}
if ("startDate" in emp) {
console.log(`Start Date: ${emp.startDate}`);
}
}
# 类型保护-typeof
代码语言:javascript复制function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding 1).join(" ") value;
}
if (typeof padding === "string") {
return padding value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
# 类型保护-instanceof
代码语言:javascript复制interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}
# 自定义类型保护的类型谓词
代码语言:javascript复制function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
# _数字分隔符
代码语言:javascript复制const inhabitantsOfMunich = 1_558_395;
const fileSystemPermission = 0b111_111_000;
// "use strict";
// const inhabitantsOfMunich = 1558395;
// const fileSystemPermission = 504;
# 使用限制
- 只能在两个数字之间添加分割符
1_000_000
是有效的1_000_000_
或_1_000_000
是非法的3._14
或3_.14
是非法的1_e10
或1e_10
是非法的1__1
连续分割符也是非法的
# 解析分隔符
以下用于解析数字的函数是不支持分隔符的:
Number()
parseInt()
parseFloat()
Number('123_456_789'); // NaN
parseInt('123_456_789'); // 123
parseFloat('123_456_789'); // 123
处理分隔符:
代码语言:javascript复制const RE_NON_DIGIT = /[^0-9]/gu;
function parseNumber(str: string) {
return Number(str.replace(RE_NON_DIGIT, ''));
}
parseNumber('123_456_789'); // 123456789
parseNumber('1,234,567,890'); // 1234567890
parseNumber('3.14'); // 314
# @xx装饰器
装饰器本质是一个函数,通过装饰器可以方便地定义与对象相关的元数据。
# 语法
代码语言:javascript复制@Plugin({
name: 'my-plugin',
version: '1.0.0',
})
@Injectable()
export class Device extends IonicNativePlugin {}
# 分类
类装饰器
- 声明
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
- 例子
function Greeter(targte: Function): void {
targte.prototype.greet = function (): void {
console.log("Hello Cell");
};
}
@Greeter
class Greeting {
constructor() {}
}
const myGreeting = new Greeting();
(myGreeting as any).greet(); // Hello Cell
属性装饰器
- 声明
declare type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void;
- 例子
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true,
});
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Person {
@logProperty
public name: string;
constructor(name: string) {
this.name = name;
}
}
const p = new Person("Cell");
// Set: name => Cell
p.name = "Cellinlab"; // Set: name => Cellinlab
方法装饰器
- 声明
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>
) => TypedPropertyDescriptor<T> | void;
- 例子
function logFunc(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("before invoking " propertyKey);
let result = originalMethod.apply(this, args);
console.log("after invoking " propertyKey);
return result;
};
}
class Task {
@logFunc
runTask(arg: any): any {
console.log("running task with " arg);
return "finished";
}
}
let task = new Task();
let result = task.runTask("Learn TS");
console.log(result);
// before invoking runTask
// running task with Learn TS
// after invoking runTask
// finished
参数装饰器
- 声明
declare type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
- 例子
function logParams(
target: Object,
key: string,
parameterIndex: number
) {
let functionLogged = key || target.prototype.constructor.name;
console.log(
`The parameter in position ${parameterIndex} at ${functionLogged} has been decorated`
);
}
class Greeter {
greeting: string;
constructor(@logParams message: string) {
this.greeting = message;
}
}
// The parameter in position 0 at Greeter has been decorated
# #xxx私有属性
代码语言:javascript复制class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let p = new Person("Cell");
p.greet(); // Hello, my name is Cell!
console.log(p.#name); // Property '#name' is not accessible outside class 'Person' because it has a private identifier.
私有字段规则:
- 私有字段必须以
#
开头 - 每个私有字段名称都唯一地限定于其包含的类
- 不能在私有字段上使用可访问性修饰符(
public
、private
) - 私有字段不能在包含的类之外访问,甚至不能被检测到
# 私有字段与private的区别
- 私有字段利用
WeakMap
来存储数据 private
可以绕过语法检查,但是私有字段不行