TypeScript 进阶 高级类型

发布于 2024-09-07 09:59:36 字数 24638 浏览 22 评论 0

& 交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型,它包含了所需的所有类型的特性。

例如, Person & Serializable & Loggable 同时是 Person Serializable Loggable 。 就是说这个类型的对象同时拥有了这三种类型的成员。大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

| 联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。联合类型适合于那些值可以为不同类型的情况

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

/**
 * 获取一个字符串并向左添加“ padding”。
 * 如果‘ padding’是一个字符串,那么‘ padding’就会附加到左侧。
 * 如果‘ padding’是一个数字,那么这个数字将被添加到左侧。
 */
function padLeft(value: string, padding: any) {
    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}'.`);
}

padLeft("Hello world", 4); // 返回 "    Hello world"

padLeft 存在一个问题, padding 参数的类型指定成了 any 。 这就是说我们可以传入一个既不是 number 也不是 string 类型的参数,但是 TypeScript 却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

代替 any , 我们可以使用 联合类型 做为 padding 的参数:

function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // 编译过程中的错误

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay,因为是共有的
pet.swim();    // errors

这个例子里, Bird 具有一个 fly 成员。 我们不能确定一个 Bird | Fish 类型的变量是否有 fly 方法。 如果变量在运行时是 Fish 类型,那么调用 pet.fly() 就出错了。

类型保护与区分类型(Type Guards and Differentiating Types)

用户自定义的类型保护

类型保护就是一些表达式 ,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

// pet is Fish 就是类型谓词。 谓词为 parameterName is Type 这种形式, parameterName 必须是来自于当前函数签名里的一个参数名。
function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

// 每当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

TypeScript 不仅知道在 if 分支里 petFish 类型; 它还清楚知道在 else 分支里,一定 不是 Fish 类型,一定是 Bird 类型。

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护

这些 typeof 类型保护 只有两种形式能被识别: typeof v === "typename"typeof v !== "typename""typename" 必须是 "number""string""boolean""symbol"

但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

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 类型保护

instanceof 类型保护 是通过 构造函数 来细化类型的一种方式。

instanceof 的右侧要求是一个构造函数 ,TypeScript 将细化为:

  1. 此构造函数的 prototype 属性的类型,如果它的类型不为 any 的话
  2. 构造签名所返回的类型的联合
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;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为 SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

可以为 null 的类型

TypeScript 具有两种特殊的类型, nullundefined ,它们分别具有值 null 和 undefined.

默认情况下,类型检查器认为 nullundefined 可以赋值给任何类型。 nullundefined 是所有其它类型的一个有效值。

你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。

--strictNullChecks 标记可以解决此错误 :当你声明一个变量时,它不会自动地包含 nullundefined 。使用联合类型明确的包含它们:

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

TypeScript 会把 nullundefined 区别对待。 string | nullstring | undefinedstring | undefined | null 是不同的类型。

使用了 --strictNullChecks可选参数可选属性 会被自动地加上 | undefined:

// 可选参数
function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

// 可选属性
class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型保护和类型断言

由于可以为 null 的类型是通过联合类型实现,那么你需要使用类型保护来去除 null 。这与在 JavaScript 里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}
//上面代码很明显地去除了 null,你也可以使用短路运算符:
function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除 nullundefined (如编译器无法去除嵌套函数的 null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name 的类型。),你可以使用类型断言手动去除。 语法是添加 ! 后缀: identifier!identifier 的类型里去除了 nullundefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' 可能是 null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok,使用类型断言手动去除
  }
  name = name || "Bob";
  return postfix("great");
}

type 类型别名

类型别名会给一个类型起个新名字,多用于符合类型。起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

同接口一样, 类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入

type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

与交叉类型一起使用 ,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

类型别名不能出现在声明右侧的任何地方

type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

  1. 接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。在编译器中将鼠标悬停在 interfaced 上,显示它返回的是 Interface ,但悬停在 aliased 上时,显示的却是对象字面量类型
    type Alias = { num: number }
    interface Interface {
        num: number;
    }
    declare function aliased(arg: Alias): Alias;
    declare function interfaced(arg: Interface): Interface;
    
  1. 类型别名不能被 extendsimplements (自己也不能 extendsimplements 其它类型)

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值

在实际应用中, 字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合 。 通过结合使用这些特性,你可以 实现类似枚举类型的字符串

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here,只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误

字符串字面量类型还可以用于区分函数重载

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

数字字面量类型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}
# 它们可以用在缩小范围调试 bug 的时候:
function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.当 x 与 2 进行比较的时候,它的值必须为 1,这就意味着上面的比较检查是非法的。
    }
}

枚举成员类型

当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的

可辨识联合(Discriminated Unions)

合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合代数数据类型 。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而 TypeScript 则基于已有的 JavaScript 模式。 它具有 3 个要素

  1. 具有普通的单例类型属性— 可辨识的特征
  2. 一个类型别名包含了那些类型的联合— 联合
  3. 此属性上的类型保护。
// 1. 每个接口都有 kind 属性但有不同的字符串字面量类型。 kind 属性称做 可辨识的特征或 标签。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

// 2.把它们联合到一起
type Shape = Square | Rectangle | Circle;

// 3.使用可辨识联合:
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。

比如,如果我们添加了 TriangleShape ,我们同时还需要更新 area :

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

有两种方式可以实现:

  1. 首先是启用 --strictNullChecks 并且指定一个返回值类型
function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}
// 因为 `switch`没有包涵所有情况,所以 TypeScript 认为这个函数有时候会返回 `undefined`。 如果你明确地指定了返回值类型为 `number`,那么你会看到一个错误,因为实际上返回值的类型为 `number | undefined`。 然而,这种方法存在些微妙之处且 `--strictNullChecks`对旧代码支持不好。
  1. 第二种方法使用 never 类型,编译器用它来进行完整性检查
function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}
// 这里, `assertNever`检查 `s`是否为 `never`类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个 case,那么 `s`将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个 case 的时候也更加明显。

多态的 this 类型

多态的 this 类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded 多态性。

比如。 在计算器的例子里,在每个操作之后都返回 this 类型的链式调用:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

# 由于这个类使用了 `this`类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();
// 如果没有 this 类型, ScientificCalculator 就不能够在继承 BasicCalculator 的同时还保持接口的连贯性。 multiply 将会返回 BasicCalculator,它并没有 sin 方法。 然而,使用 this 类型, multiply 会返回 this,在这里就是 ScientificCalculator。

索引类型(Index types)

使用索引类型,编译器就能够检查使用了动态属性名的代码。

通过 索引类型查询索引访问 操作符:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // 编译器会检查 name 是否真的是 Person 的一个属性 ok, string[]

索引类型查询操作符keyof T ,对于任何类型 Tkeyof T 的结果为 T 上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // 'name' | 'age'

keyof Person 是完全可以与 'name' | 'age' 互相替换的。 不同的是如果你添加了其它的属性到 Person ,例如 address: string ,那么 keyof Person 会自动变为 'name' | 'age' | 'address' 。 你可以在像 pluck 函数这类上下文里使用 keyof ,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

索引访问操作符T[K] ,只要确保类型变量 K extends keyof T 就可以了

//  o: T 和 name: K,意味着 o[name]: T[K]
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

索引类型和字符串索引签名

keyofT[K] 与字符串索引签名进行交互。

如果你有一个带有字符串索引签名的类型,那么 keyof T 会是 string

并且 T[string] 为索引签名的类型

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

映射类型

从旧类型中创建新类型的一种方式 — 映射类型 ,在映射类型里,新类型以相同的形式去转换旧类型里每个属性。

例如,你可以令每个属性成为 readonly 类型或可选的:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
//interface PersonReadonly {
//    readonly name: string;
//    readonly age: number;
//}

type Partial<T> = {
    [P in keyof T]?: T[P];
}
//interface PersonPartial {
//    name?: string;
//    age?: number;
//}

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

// Keys 是硬编码的的属性名列表并且属性类型永远是 boolean,因此这个映射类型等同于:
type Flags = {
    option1: boolean;
    option2: boolean;
}

它的语法与索引签名的语法类型,内部使用了 for .. in 。 具有三个部分:

  1. 类型变量 K ,它会依次绑定到每个属性。
  2. 字符串字面量联合的 Keys ,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

在真正的应用里,可能不同于上面的 ReadonlyPartial 。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof 和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

假设 Person.name 是只读的,那么 Partial<Person>.name 也将是只读的且为可选的。

下面是另一个例子, T[P] 被包装在 Proxy<T> 类里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

ReadonlyPartialPick 是同态的,但 Record 不是。 因为 Record 并不需要输入类型来拷贝属性,所以它不属于同态,非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

由映射类型进行推断

拆包

// 注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

预定义的有条件类型

TypeScript 2.8 在 lib.d.ts 里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从 T 中剔除可以赋值给 U 的类型。
  • Extract<T, U> -- 提取 T 中可以赋值给 U 的类型。
  • NonNullable<T> -- 从 T 中剔除 nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

示例

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

小结

  1. 交叉类型是多种类型的合并
  2. 联合类型表示一个值可以是几种类型之一。如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
  3. 类型谓词为 parameterName is Type 这种形式, parameterName 必须是来自于当前函数签名里的一个参数名。
  4. typeof 类型保护 只有两种形式能被识别: typeof v === "typename"typeof v !== "typename""typename" 必须是 "number""string""boolean""symbol"
  5. TypeScript 会把 nullundefined 区别对待。 string | nullstring | undefinedstring | undefined | null 是不同的类型。
  6. 使用 --strictNullChecks 标记时,当你声明一个变量时,它不会自动地包含 nullundefined
  7. 使用了 --strictNullChecks可选参数可选属性 会被自动地加上 | undefined:
  8. 如果编译器不能够去除 nullundefined (如编译器无法去除嵌套函数的 null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name 的类型。),你可以使用类型断言手动去除。 语法是添加 ! 后缀: identifier!identifier 的类型里去除了 nullundefined 4
  9. 类型别名会给一个类型起个新名字。起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型;类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入;类型别名不能出现在声明右侧的任何地方。
  10. 类型别名不能被 extendsimplements (自己也不能 extendsimplements 其它类型)
  11. 如果你无法通过 接口 来描述一个类型并且需要使用 联合类型元组类型,这时通常会使用类型别名
  12. 字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合,来实现类似枚举类型的字符串。另外,字符串字面量类型还可以用于区分函数重载。
  13. 索引类型查询操作符keyof T ,对于任何类型 Tkeyof T 的结果为 T 上已知的公共属性名的联合。
  14. 索引访问操作符T[K] ,只要确保类型变量 K extends keyof T 就可以了

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

別甾虛僞

暂无简介

文章
评论
28 人气
更多

推荐作者

十二

文章 0 评论 0

飞烟轻若梦

文章 0 评论 0

OPleyuhuo

文章 0 评论 0

wxb0109

文章 0 评论 0

旧城空念

文章 0 评论 0

-小熊_

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文