「Typescript系列」三千字带你学习TS类型操作

558 阅读9分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

前言

阅读部分:Typescript官方手册---Type Manipulation类型操作

欢迎加入ts对赌学习,本文为第一天&第二天学习总结。

TypeScript 的类型系统非常强大,因为它允许根据其他类型 来表达类型

(一)泛型

我对泛型的理解:不需要用户指定特定的类型,可以通过编程的方式,让程序自己能推导出应该是什么类型 的一种不确定的类型

我们需要的,是一种捕获参数类型的方法,以便我们也可以使用它来表示返回的内容。

举一个例子:

 //我们使用一个泛型Type
 function identity<Type>(arg: Type): Type {
   return arg;
 }
 //当我们调用是传入Type的类型应该是number时
 //程序会规定arg以及函数返回的类型为number
 let res = identity<number>(1);
 ​
 //如果不显式地指定类型,编译器也会进行推断
 let res = identity(1);

在Typescript中,允许我们将泛型类型变量作为我们使用类型的一部分,而不是整一个类型,提供了更大的方便性,例如,我们可以把Type作为数组元素的类型,返回一个该类型的数组,而不是该类型。

 function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
   console.log(arg.length); // Array有.length,所以不会报错
   return arg;
 }

通用类型

我们可以定义一个通用接口,把这个泛型抽离出来,形成通用类型

 interface GenericIdentityFn {
   <Type>(arg: Type): Type;
 }
  
 //使Type可以看成是整个接口的参数
 //使得类型参数对接口的所有其他成员可见
 interface GenericIdentityFn<Type> {
   (arg: Type): Type;
 }
 ​
 function identity<Type>(arg: Type): Type {
   return arg;
 }
  
 let myIdentity: GenericIdentityFn = identity;

通用类

泛型类和泛型接口很相似,泛型类<>在类名后面的 ( ) 中,包含了泛型类型的参数列表

 class GenericNumber<NumType> {
   zeroValue: NumType;
   add: (x: NumType, y: NumType) => NumType;
 }

你可以对NumType传出number或string或者任意类型,就像接口一样,将类型参数放在类本身上可以确保类的所有属性都使用相同的类型。

通用约束

当我们希望对用户传进来的Type有所约束时,我们可以通过extends来进行约束

 interface Lengthwise {
   length: number;
 }
  
 function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
   console.log(arg.length); 
   return arg;
 }
 ​
 //类型“number”不满足约束“Lengthwise”。
 loggingIdentity<number>(3);
 //ok
 loggingIdentity({ length: 10, value: 3 });

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

举个例子,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取到不存在的属性,因此我们将在两种类型之间放置一个约束。

 //Key extends keyof Type 
 //代表传入的key必须包含在Type说返回的所有key之中
 function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
   return obj[key];
 }
  
 let x = { a: 1, b: 2, c: 3, d: 4 };
  
 //ok
 getProperty(x, "a");
 //error,类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数
 getProperty(x, "m");

在泛型中使用类类型

 class BeeKeeper {
   hasMask: boolean = true;
 }
  
 class ZooKeeper {
   nametag: string = "Mikle";
 }
  
 class Animal {
   numLegs: number = 4;
 }
  
 class Bee extends Animal {
   keeper: BeeKeeper = new BeeKeeper();
 }
  
 class Lion extends Animal {
   keeper: ZooKeeper = new ZooKeeper();
 }
  
 function createInstance<A extends Animal>(c: new () => A): A {
   return new c();
 }
  
 createInstance(Lion).keeper.nametag;
 createInstance(Bee).keeper.hasMask;

(二)Keyof 类型运算符

TypeScript 允许我们遍历某种类型的属性,并通过 keyof 操作符提取其属性的名称。

 interface Person {
   name: string;
   age: number;
   location: string;
 }
 ​
 type K1 = keyof Person; // "name" | "age" | "location"
 type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ...
 type K3 = keyof { [x: string]: Person };  // string | number

除了接口外,keyof 也可以用于操作类,比如:

 class Person {
   name: string = "Semlinker";
 }
 ​
 let sname: keyof Person;
 sname = "name";

若把 sname = "name" 改为 sname = "age" 的话,TypeScript 编译器会提示以下错误信息:不能将类型“"age"”分配给类型“"name"”

实战场景

我们希望定义一个函数,传入一个对象和key值,返回对应value

 function prop(obj, key) {
   return obj[key];
 }

我们希望实现这样子的效果,该函数用于获取 某个对象中指定属性的属性值 。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof 操作符:

 function prop<T extends object, K extends keyof T>(obj: T, key: K) {
   return obj[key];
 }

首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型。

来源:semlinker.com/ts-keyof/

(三)Typeof 类型运算符

在 TypeScript 中,typeof 操作符可以用来获取一个变量或对象的类型。

 let s = "hello";
 let n: typeof s;
 //n:string
 ​
 const kakuqo = {
     name: "kakuqo",
     age: 30,
     address: {
       province: '福建',
       city: '厦门'   
     }
 }
 ​
 type Kakuqo = typeof kakuqo;
 /*
  type Kakuqo = {
     name: string;
     age: number;
     address: {
         province: string;
         city: string;
     };
 }
 */

此外,typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:

 function toArray(x: number): Array<number> {
   return [x];
 }
 ​
 type Func = typeof toArray; // -> (x: number) => number[]

来源:www.semlinker.com/ts-typeof/

(四)索引访问类型

我们可以使用索引访问类型来查找另一种类型的特定属性

 type Person = { age: number; name: string; alive: boolean };
 type Age = Person["age"]; //number
 type Age = Person[keyof Person]; //string | number | boolean

我们可以通过 number获取数组(元组)元素的类型(联合类型):

 const MyArray = [
   { name: "Alice", age: 15 },
   { name: "Bob", age: 23 },
   { name: "Eve", age: 38 },
 ];
  
 type Person = typeof MyArray[number]; 
 /*
 type Person = {
     name: string;
     age: number;
 }
 */
 const MyArray = [
   { name: "Alice", age: 15 },
   { name: "Bob", height: "173cm" },
   { name: "Eve", age: 38 },
 ];
 ​
 type Person = typeof MyArray[number]; 
 /*
 type Person = {
     name: string;
     age: number;
     height?: undefined;
 } | {
     name: string;
     height: string;
     age?: undefined;
 }
 */

只能用类型变量进行索引查找:

 // 错误
 // const key = "age";
 // type Age = Person[key];
 ​
 //js和ts的内容不能混用
 //[]中得使用一个type变量
 const key = "age";
 type Age = Person[typeof key];

(五)条件类型

我们可以根据传进去的泛型T,通过逻辑判断,得到一个真实的类型:

 interface Animal {
   live(): void;
 }
 interface Dog extends Animal {
   woof(): void;
 }
 ​
 // Example1 : number
 type Example1 = Dog extends Animal ? number : string;
 // Example2 : string
 type Example2 = RegExp extends Animal ? number : string;

函数重载

同样的,在函数重载方面,条件类型也发挥了很大的作用:

 interface IdLabel {
   id: number /* some fields */;
 }
 interface NameLabel {
   name: string /* other fields */;
 }
 ​
 //不使用泛型,不够简洁,需要写出所有情况
 function createLabel(id: number): IdLabel;
 function createLabel(name: string): NameLabel;
 function createLabel(nameOrId: string | number): IdLabel | NameLabel;
 function createLabel(nameOrId: string | number): IdLabel | NameLabel {
   throw "unimplemented";
 }
 ​
 //泛型条件判断,如果T是number那么NameOrId就是IdLabel类型
 //反之是NameLabel类型
 type NameOrId<T extends number | string> = T extends number
   ? IdLabel
   : NameLabel;
 ​
 //以此为例子
 //该函数接受number或string类型的参数,对应的返回值是IdLabel或者NameLabel
 function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
   throw "unimplemented";
 }
 ​
 let a = createLabel("typescript");
 let b = createLabel(2.8);
 let c = createLabel(Math.random() ? "hello" : 42);
 ​

条件类型约束

 //我们约束:T必须包含一个名为message的属性,该类型返回message的类型
 //其实完成了两件事:一件事是对传入T的约束,一件事是对自身类型的控制
 type MessageOf<T extends { message: unknown }> =? T["message"] : never;
  
 interface Email {
   message: string;
 }
 ​
 //EmailMessageContents : string
 type EmailMessageContents = MessageOf<Email>;
 ​
 interface Dog {
   bark(): void;
 }
 ​
 //DogMessageContents : never 因为Dog接口中未包含message这个属性
 type DogMessageContents = MessageOf<Dog>;
 ​

另外一个例子,实现一下扁平化数组的功能

 type Flatten<T> = T extends any[] ? T[number] : T;
  
 // 当传进去是一个数组类型时,返回数组元素类型
 type Str = Flatten<string[]>;
 ​
 // 当传进去一个元素类型是,返回本身
 type Num = Flatten<number>;

在条件类型中推断

另外,ts还给我们提供了一种用法

 type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

使用 infer 声明性地引入了一个新的泛型类型变量 Item,而不是指定如何在 true 分支中检索 T 的元素类型。

这样子我们可以使用infer编写一些有用的类型别名,例如,提取函数类型中的返回类型

 type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
   ? Return
   : never;

当存在多个重载函数时,对最后一个函数进行推导

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

分配条件类型

 type ToArray<Type> = Type extends any ? Type[] : never;

有这么一种情况,当Type传进一个联合类型

 type StrArrOrNumArr = ToArray<string | number>;

StrArrOrNumArr 得到的结果是 string[] | number[]

如果我们想变成 (string | number)[] 类型,可以用方括号将 extends 关键字的每一边包围起来。

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

(六)映射类型

有时候我们不希望返回本身,有时候希望可以改变成为一种类型。

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

 type OptionsFlags<Type> = {
   [Property in keyof Type]: boolean;
 };
 ​
 type FeatureFlags = {
   darkMode: () => void;
   newUserProfile: () => void;
 };
 ​
 //将所有属性均变为boolean类型
 type FeatureOptions = OptionsFlags<FeatureFlags>;
 /*
 type FeatureOptions = {
     darkMode: boolean;
     newUserProfile: boolean;
 }
 */

映射修饰符

在映射过程中可以使用两个额外的修饰符: readonly? ,它们分别影响可变性和可选性。

另外,可以通过使用 -+ 作为前缀来删除或添加这些修饰符。如果没有添加前缀,则假定为 + 。

 //去除readonly
 type CreateMutable<Type> = {
   -readonly [Property in keyof Type]: Type[Property];
 };
 ​
 type LockedAccount = {
   readonly id: string;
   readonly name: string;
 };
 ​
 type UnlockedAccount = CreateMutable<LockedAccount>;
 /*
 UnlockedAccount : {
     id: string;
     name: string;
 }
 */
 //去除可选性质
 type Concrete<Type> = {
   [Property in keyof Type]-?: Type[Property];
 };
  
 type MaybeUser = {
   id: string;
   name?: string;
   age?: number;
 };
  
 type User = Concrete<MaybeUser>;
 /*
   type User = {
       id: string;
       name: string;
       age: number;
   }
 */

使用as对Key重新映射

可以使用映射类型中的 as 子句重新映射映射类型的键,例如对key的名称进行更改:

 //注意使用反引号
 type Getters<Type> = {
     [Property in keyof Type as /`get${Capitalize<string & Property>}/`]: () => Type[Property]
 };
 ​
 interface Person {
     name: string;
     age: number;
     location: string;
 }
  
 type LazyPerson = Getters<Person>;
 /*    
 type LazyPerson = {
     getName: () => string;
     getAge: () => number;
     getLocation: () => string;
 }
 */

另一个例子

 //指定Type中属性kind的值作为新的属性名
 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 = EventConfig<SquareEvent | CircleEvent>
 /*
 type Config = {
     square: (event: SquareEvent) => void;
     circle: (event: CircleEvent) => void;
 }
 */

(七)模板文字类型

模板文字类型建立在字符串文字类型之上,并且能够通过联合扩展成许多字符串。

与 JavaScript 中的模板字符串具有相同的语法,但是用于类型位置。当与具体文本类型一起使用时,模板文本通过连接内容生成一个新的字符串文本类型。

 type EmailLocaleIDs = "welcome_email" | "email_heading";
 type FooterLocaleIDs = "footer_title" | "footer_sendoff";
  
 //type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
 type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
 ​
 ​
 //对于模板文字中的每个插值位置,联合是十字乘:
 type Lang = "en" | "ja" | "pt";
 ​
 //type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
 type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

类型字符串联合

将字符串和类型进行联合使用,将碰撞出不一样的火花。

 //该类型存在一个on方法,名称为每一个属性名+Changed
 type PropEventSource<Type> = {
   on(
     eventName: `${string & keyof Type}Changed`,
     callback: (newValue: Type[Key]) => void
   ): void;
 };
 ​
 //我们声明一个方法,将原类型和上面的类型合并(&)起来
 declare function makeWatchedObject<Type>(
   obj: Type
 ): Type & PropEventSource<Type>;
 ​
 //创建一个实例
 const person = makeWatchedObject({
   firstName: "Saoirse",
   lastName: "Ronan",
   age: 26
 });
 ​
 person.on("firstNameChanged", () => {});

这样子我们可以在使用时,给予我们代码提示,这是我们希望的。

image-20211120162107337

image-20211120162126468

内部字符串操作类型

 Uppercase<StringType> //转大写
 Lowercase<StringType> //转小写
 Capitalize<StringType> //首字母大写
 Uncapitalize<StringType>//首字母小写

至此,这部分的内容就学完啦~可以摸会鱼了。

持续更新中~欢迎关注我的掘金和github,觉得不错的话,记得给我的项目🌟 一下哦~