【TypeScript】深入学习TypeScript类型操作

目录

  • 前言
  • 1、泛型
    • 泛型类型
    • 泛型类
    • 泛型约束
    • 在泛型中使用类类型
  • 2、keyof类型操作符
  • 3、typeof类型操作符
  • 4、索引访问类型
  • 5、条件类型
    • 配合泛型使用
    • 分布式条件类型
  • 6、映射类型
    • 映射修改器
    • 通过as做key重映射
    • 进一步探索
  • 结语

前言

最近博主一直在创作TypeScript的内容,所有的TypeScript文章都在我的TypeScript专栏里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅

TypeScript的类型系统允许用其他类型的术语来表达类型。

通过结合各种类型操作符,我们可以用一种简洁、可维护的方式来表达复杂的操作和值。在本篇文章中,我们将介绍用现有的类型或值来表达一个新类型的方法:

  • 泛型型 :带参数的类型
  • Keyof 类型操作符keyof 操作符创建新类型
  • Typeof 类型操作符 : 使用 typeof 操作符来创建新的类型
  • 索引访问类型 :使用 Type['a'] 语法来访问一个类型的子集
  • 条件类型 :在类型系统中像if语句一样行事的类型
  • 映射类型 :通过映射现有类型中的每个属性来创建类型
  • 模板字面量类型 :通过模板字面字符串改变属性的映射类型

1、泛型

在TypeScript专栏中的前几篇文章中,我们以及大致了解了泛型的基本使用,见:

  • 【TypeScript】深入学习TypeScript对象类型中泛型对象类型
  • 【TypeScript】深入学习TypeScript函数中泛型函数(通用函数)

在这一节中我们将对泛型进行进一步的补充

泛型类型

在【TypeScript】深入学习TypeScript函数泛型函数(通用函数) 中我们创建了在一系列类型上工作的通用函数,在这一节中,我们将探讨函数本身的类型以及如何创建通用接口

泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:

  • 泛型函数的类型格式:(param:TypeToParamType) => TypeToReturnType
  • 普通函数类型格式:(param:paramType) => returnType

先看一个我们之前定义过的一个通用函数:

function getFirstElement<Type>(arr: Type[]): Type | undefined {
    return arr[0];
}

它的类型就是(arr: Type[]) => Type | undefined,我们可以将它赋值给同类型的函数fn

let fn: <Type>(arr: Type[]) => Type | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));

我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致即可:

let fn: <FnType>(fnArr: FnType[]) => FnType | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));

我们也可以把泛型写成一个对象字面类型的调用签名:

let fn: { <FnType>(fnArr: FnType[]): FnType | undefined } = getFirstElement;
console.log(fn<number>([1, 2, 3]));

这时可以将对象字面类型移到一个接口中:

interface Ifn {
    <FnType>(fnArr: FnType[]): FnType | undefined;
}
let fn: Ifn = getFirstElement;
console.log(fn<number>([1, 2, 3]));
  • 在一些情况下,我们还可以将通用参数移到整个接口的参数上,这使得我们可以看到我们的泛型是什么类型(例如Ifn而不仅仅是Ifn),使得类型参数对接口的所有其它成员可见:

    interface Ifn<FnType> {
        (fnArr: FnType[]): FnType | undefined;
    }
    let strFn: Ifn<string> = getFirstElement;
    console.log(strFn(["1", "2", "3"]));
    console.log(strFn([1, 2, 3])); // err:不能将类型“number”分配给类型“string”
    

    注意:这里的例子已经变了,不再是简单的将getFirstElement函数直接赋值给另一个函数,而是将类型参数为stringgetFirstElement函数赋值给strFn

    上述strFn相当于fn

    interface Ifn {
        <FnType>(fnArr: FnType[]): FnType | undefined;
    }
    let fn: Ifn = getFirstElement;
    console.log(fn<string>(["1", "2", "3"]));
    

泛型类

泛型类在类的名字后面有一个角括号(<>)中的泛型参数列表:

class Add<AddType> {
    initVal: AddType| undefined;
    add: ((x: AddType, y: AddType) => AddType) | undefined;
}

使用:

let myNumber = new Add<number>();
myNumber.initVal = 1;
myNumber.add = function (x, y) {
    return x + y;
};
console.log(myNumber.add(myNumber.initVal, 18)); // 19
let myStr = new Add<string>();
myStr.initVal = "Ailjx";
myStr.add = function (x, y) {
    return x + y;
};
console.log(myStr.add(myStr.initVal, " OK")); // Ailjx OK

就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。

注意:一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数

泛型约束

在【TypeScript】深入学习TypeScript函数泛型函数(通用函数) 中我们已经了解过了使用extends约束泛型,这一节我们继续深入

在泛型约束中使用类型参数

你可以声明一个受另一个类型参数约束的类型参数。

例如,我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}

keyof 运算符接收一个对象类型,并产生其键的字符串或数字字面联合,下面会详细讲解

在这里插入图片描述

在泛型中使用类类型

TypeScript中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型,比如说:

function create<Type>(c: new () => Type): Type {
    return new c();
}

create函数代表接收一个构造函数,并返回其实例

参数c的类型使用的是构造签名,表示其接收一个构造函数,并且该构造函数实例的类型(Type)被当作了create函数的类型参数并在其它地方进行使用,如create的返回值类型就是引用了Type

一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系:

class Animal {
    numLegs: number = 4;
}

class Bee extends Animal {
    name: string = "Bee";
    getName() {
        console.log(this.name);
    }
}

class Lion extends Animal {
    name: string = "Lion";
    getName() {
        console.log(this.name);
    }
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Bee).getName(); // Bee
createInstance(Lion).getName(); // Lion

这里的createInstance函数表示只能接收一个实例类型受限于Animal的构造函数,并返回其实例

【TypeScript】深入学习TypeScript类型操作_第1张图片

2、keyof类型操作符

keyof 运算符接收一个对象类型,并产生其的字符串或数字字面联合:

type ObjType = { x: number; y: number };

const p1: keyof ObjType = "x";
// 相当于
// const p1: "x" | "y" = "x";

如果该类型有一个字符串或数字索引签名keyof 将返回这些类型:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // A为 number
const a: A = 1;

type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // M为 string|number
const m: M = "a";
const m2: M = 10;

注意:在这个例子中, Mstring|number ——这是因为JavaScript对象的键总是被强制为字符串,所以 obj[0] 总是与obj["0"] 相同。

3、typeof类型操作符

JavaScript中可以使用typeof操作符获取某一变量的类型,在TypeScript中我们可以使用它来在类型上下文中引用一个变量或属性的类型:

let s = "hello";
let n: typeof s; // n类型为string
n = "world";
n = 100; // err:不能将类型“number”分配给类型“string”

结合其他类型操作符,你可以使用typeof来方便地表达许多模式。

例如我们想要获取函数返回值的类型:

  • TypeScript中内置的类型ReturnType 接收一个函数类型并产生其返回类型:

    type Predicate = (x: unknown) => boolean;
    type K = ReturnType<Predicate>; // k为boolean
    
  • 如果直接在一个函数名上使用 ReturnType ,我们会看到一个指示性的错误:

    【TypeScript】深入学习TypeScript类型操作_第2张图片
    为了指代值f的类型,我们使用 typeof

    function f() {
        return { x: 10, y: 3 };
    }
    type P = ReturnType<typeof f>; // P为{ x: number, y: number }
    

只有在标识符(即变量名)或其属性上使用typeof是合法的

4、索引访问类型

可以使用一个索引访问类型来查询一个类型上的特定属性的类型:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // Age类型为number

还可以配合联合类型 unionskeyof 或者其他类型进行使用:

interface Person {
    name: string;
    age: number;
    alive: boolean;
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11: I1 = 100;
const i12: I1 = "";

// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21: I2 = "";
const i22: I2 = 100;
const i23: I2 = false;

将索引访问类型和typeofnumber 结合起来,方便地获取一个数组字面的元素类型:

const MyArray = [
    { name: "Alice", age: 15 },
    { name: "Bob", age: 23 },
    { name: "Eve", age: 38 },
];

/* type Person = {
    name: string;
    age: number;
    } */
type Person = typeof MyArray[number];
const p: Person = {
    name: "xiaoqian",
    age: 11,
};

// type Age = number
type Age = typeof MyArray[number]["age"];
const age: Age = 11;

// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2: Age2 = 11;

注意:

  • 在索引时只能使用类型引用,不能使用变量引用:
    【TypeScript】深入学习TypeScript类型操作_第3张图片

  • 可以使用类型别名来实现类似风格的重构:

    type key = "age";
    type Age = Person[key];
    

5、条件类型

TypeScript我们可以使用三元表达式来判断一个类型:

interface Animal {}
interface Dog extends Animal {}

// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;

条件类型表达式是通过extends进行约束和判断

配合泛型使用

先看一个简单的例子:

type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;

Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取数组的元素类型。否则,它只是返回它被赋予的类型。

在看一个复杂的例子,实现一个获取idname的对象格式的函数getIdOrNameObj

interface IId {
    id: number;
}
interface IName {
    name: string;
}
// 条件类型配合泛型对类型进行判断和选择
type IdOrName<T extends number | string> = T extends number ? IId : IName;

function getIdOrNameObj<T extends number | string>(idOrName: T): IdOrName<T> {
    if (typeof idOrName === "number") {
        return {
            id: idOrName,
        } as IdOrName<T>;
    } else {
        return {
            name: idOrName,
        } as IdOrName<T>;
    }
}

const myId = getIdOrNameObj(1); // myId类型为IId
const myName = getIdOrNameObj("Ailjx"); // myName类型为IName
```### 类型推理
在条件类型的 `extends`子句中我们可以使用 `infer` 声明来推断元素类型
> `infer` 声明只能在条件类型的 `extends`子句中使用

例如,我们使用`infer` 关键字来改写上面的`Flatten````typescript
type Flatten<T> = T extends Array<infer Item> ? Item : T;

// type Str = string
type Str = Flatten<string[]>;
// type Str = number
type Num = Flatten<number[]>;

这里使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量

这里infer Item相当于一个占位,它暂时代表Array中元素的类型,当Flatten类型参数被赋值为数组后,TypeScript就会自动推断出extends语句中Array中元素的类型,这时infer Item这个占位就指向了数组元素的类型,之后就能直接使用Item来代指数组元素的类型了

这使得我们不用再使用索引访问类型T[number] "手动 "提取数组元素的类型了

使用 infer 关键字从函数类型中提取出返回类型:

// 当GetReturnType接收类型为函数签名时返回函数返回值类型,否者直接返回接收的类型
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
    ? Return
    : Type;

// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

// type Arr=any[]
type Arr = GetReturnType<any[]>;

当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断:

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;

declare可以向TypeScript域中引入一个变量,这可以解决在重载函数只有重载签名而没有实现签名时的报错

分布式条件类型

当条件类型作用于一个通用类型时,当给定一个联合类型时,它就变成了分布式的:

type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

将一个联合类型string | number插入ToArray,那么条件类型将被应用于该联合的每个成员

  • StrArrOrNumArr分布在string | number;
  • 条件类型会对联合的每个成员类型进行映射:ToArray | ToArray
  • 最终返回string[] | number[]

取消分布式:

如果不需要分布式的这种行为,我们可以使用方括号[]包围extends关键字的两边

type ToArray<Type> = [Type] extends [any] ? Type[] : never;
// type StrArrOrNumArr = (string|number)[]
type StrArrOrNumArr = ToArray<string | number>;

6、映射类型

当一个类型可以以另一个类型为基础创建新类型。

映射类型建立在索引签名(见【TypeScript】深入学习TypeScript对象类型)的语法上

映射类型是一种通用类型,它使用 PropertyKeys 的联合(经常通过keyof 创建)迭代键来创建一个类型:

type OptionsFlags<Type> = {
    [Property in keyof Type]: boolean;
};

在这个例子中, OptionsFlags 将从Type 类型中获取所有属性,并将它们的值的类型改为boolean

type Obj = {
    name: string;
    age: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
    type FeatureOptions = {
        name: boolean;
        age: boolean;
    }
    */

映射修改器

在映射过程中,有两个额外的修饰符可以应用: readonly? ,它们分别影响可变性和可选性,可以通过用-+ 作为前缀来删除或添加这些修饰符(不加修饰符就默认是+ ):

type OptionsFlags<Type> = {
	// 删除readonly和?,readonly在前,?在后
    -readonly [Property in keyof Type]-?: boolean;
};
type Obj = {
    readonly name: string;
    age?: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
    type FeatureOptions = {
        name: boolean;
        age: boolean;
    }
    */

通过as做key重映射

TypeScript 4.1及以后的版本中,可以通过映射类型中的as子句修改映射类型中的键名:

type OptionsFlags<Type> = {
    // 将键重命名为哦、
    [Property in keyof Type as "o"]: Type[Property];
};
type Obj = {
    name: string;
    age: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
    type FeatureOptions = {
        o:string|number
    }
    */

上面是将所有键名都更改成了'o'

我们也可以利用模板字面类型,在之前的属性名称的基础上进行更改:

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property];
};
interface Person {
    name: string;
    age: number;
    location: string;
}
/*
    type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
    }
    */
type LazyPerson = Getters<Person>;
  • CapitalizeTS内置类型,能将传入的字符串类型首字母转为大写
  • string & Property通过交叉类型,确保Capitalize 接收的为字符串类型

可以通过条件类型Exclude根据键名产生never ,从而过滤掉该键:

type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property];
};

interface Circle {
    kind: "circle";
    radius: number;
}

/*
    type KindlessCircle = {
        radius: number;
    }
 */
type KindlessCircle = RemoveKindField<Circle>;

ExcludeTS内置类型:type Exclude = T extends U ? never : T

可以映射任意的联合体:

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
};
type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
/*
    type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
    }
    */
type Config = EventConfig<SquareEvent | CircleEvent>;

进一步探索

映射类型与本篇文章中指出的其他功能配合得很好,例如,下面这个使用条件类型的映射类型,它根据一个对象类型的属性show是否被设置为字面类型true而返回truefalse

type ExtractShow<Type> = {
    [Property in keyof Type]: Type[Property] extends { show: true }
        ? true
        : false;
};

type PermissionInfo = {
    home: { url: string; show: true };
    about: { url: string; show: true };
    admin: { url: string };
};

/*
    type judge = {
        home: true;
        about: true;
        admin: false;
    }
*/
type judge = ExtractShow<PermissionInfo>;

结语

至此,本篇文章内容就全部结束了,如果本篇文章对你有所帮助,还请客官一件四连!

博主的TypeScript专栏正在慢慢的补充之中,赶快关注订阅,与博主一起进步吧!期待你的三连支持。

参考资料:TypeScript官网

你可能感兴趣的:(typescript,学习,javascript)