TS 从 0 到 1 - TypeScript 中的各种符号

2023-05-17 19:58:39 浏览数 (2)

# ! 非空断言

! 后缀表达式可以用于断言操作对象是非 null 和非 undefined 类型。即 x!,将从 x 值域中排除 nullundefined

# 忽略 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 该属性会被明确地赋值。

代码语言:javascript复制
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;
}

# ?. 可选链

可选链,可以在遇到 nullundefined 立即停止某些表达式的运行。

代码语言:javascript复制
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 值,比如空字符串、0falsenull 等,而 ?. 专门用于检测 nullundefined。对于 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 而不是抛出一个异常。

代码语言:javascript复制
let result = obj.customMethod?.();

// "use strict";
// var _a;
// let result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);

  • 如果存在一个属性名且该属性名对应的值不是函数类型,使用 ?. 仍然会产生一个 TypeError 异常。
  • 可选链的运算行为被局限在属性的访问、调用以及元素的访问——不会延伸到后续的表达式中。

# ?? 空值合并

??,当左侧操作数为 nullundefined 时,其返回右侧的操作数,否则返回左侧的操作数。

|| 的区别,逻辑或会在左侧为 falsy 值是返回右侧的操作数。如果使用 || 为某些变量设置默认值时,会遇到意料之外的事,如 falsy值(空字符串、NaN0)时。

代码语言:javascript复制
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);

# 短路

当空值合并运算符的左表达式不为 nullundefined 时,不会对右表达式求值。

代码语言:javascript复制
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

# 不能与 && 或 || 操作符共用

空值合并运算符 ?? 不能与 &&|| 操作符共用,否则会产生语法错误。

代码语言:javascript复制
null || undefined ?? "foo"; // SyntaxError

true && undefined ?? "foo"; // SyntaxError

(null || undefined) ?? "foo"; // "foo" (works)

# 与可选链操作符的关系

空值合并运算符 ?? 和 可选链操作符 ?. 都是针对 undefinednull

代码语言:javascript复制
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> 工具类型,可以将所有属性设置为可选的。

代码语言:javascript复制
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> 工具类型,可以将所有可选属性设置为必选的。

代码语言:javascript复制
interface PullDownRefreshConfig {
  threshold: number;
  stop: number;
}

type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>

实现原理:

代码语言:javascript复制
type Required<T> = {
  [P in keyof T]-?: T[P];
};

通过 -? 移除了可选属性中的 ?,使得属性从可选变为必选。

# & 合并

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

代码语言:javascript复制
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,因为 stringnumber 之间没有交集。

# 同名非基础类型属性的合并

代码语言: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 中联合类型表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型。联合类型通常与 nullundefined 一起使用,表示一个值可以为 nullundefined 或某个类型。

代码语言:javascript复制
const sayHello = (name: string | undefined) => {
  if (name === undefined) {
    console.log("Hello, Stranger");
  } else {
    console.log(`Hello, ${name.toUpperCase()}`);
  }
};

  • 字面量类型
代码语言:javascript复制
// 用来约束取值只能是某几个值中的一个
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._143_.14 是非法的
    • 1_e101e_10 是非法的
    • 1__1 连续分割符也是非法的

# 解析分隔符

以下用于解析数字的函数是不支持分隔符的:

  • Number()
  • parseInt()
  • parseFloat()
代码语言:javascript复制
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 {}

# 分类

类装饰器

  • 声明
代码语言:javascript复制
declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

  • 例子
代码语言:javascript复制
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

属性装饰器

  • 声明
代码语言:javascript复制
declare type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;

  • 例子
代码语言:javascript复制
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 

方法装饰器

  • 声明
代码语言:javascript复制
declare type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypePropertyDescript<T>
) => TypedPropertyDescriptor<T> | void;

  • 例子
代码语言:javascript复制
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

参数装饰器

  • 声明
代码语言:javascript复制
declare type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;

  • 例子
代码语言:javascript复制
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.

私有字段规则:

  • 私有字段必须以#开头
  • 每个私有字段名称都唯一地限定于其包含的类
  • 不能在私有字段上使用可访问性修饰符(publicprivate
  • 私有字段不能在包含的类之外访问,甚至不能被检测到

# 私有字段与private的区别

  • 私有字段利用 WeakMap 来存储数据
  • private 可以绕过语法检查,但是私有字段不行

0 人点赞