TypeScript 类型操作

1,033 阅读5分钟

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

前言

在掌握好 TypeScript 基础之后,就需要将基础类型联合起来做出更优美的类型声明。

TypeScript 中有很多关键字,如果不去主动接触的话就会少了很多可操作性。例如:inferkeyoftypeofextends 之类的。还有映射和模板字符串这种概念

泛型

利用泛型来达到类型复用的目的,而不是一昧的用 any 大法。这里达到的效果是传入什么类型,就返回该类型

function identify<T>(arg: T): T {
  return arg;
}

接口的形式定义泛型

interface GenericIdentifyFn {
    <T>(arg: T): T
}

function identify<T>(arg: T): T {
    return arg
}

let myIdentify: GenericIdentifyFn = identify

在类型的继承中使用

class BeeKeeper {
  hasMask!: boolean;
}

class ZooKeeper {
  nametag!: string;
}

class Animal {
  numLegs!: number;
}

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;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

keyof

运算符:构造一个类型,类型由参数的所有键值

interface Person {
  name: string;
  age: number;
}
type T1 = keyof Person; // "name" | "age"
type T2 = keyof { [x: string]: 1 }; // string | number

type T3 = number;
type T4 = keyof T3; // type T4 = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

提取键值,组成类型。提取一定要是的类型

typeof

运算符:获取变量的类型

type Predicate = (x: unknown) => boolean;

function f() {
  return true;
}

type K = ReturnType<Predicate>; // type K = boolean
type T = ReturnType<typeof f>; // type T = boolean

JavaScript 中也有 typeofTypeScript 环境下,编辑器能够区分应该使用哪个

keyoftypeof 这两个都是提取类型的,一个是提取类型,一个是提取变量。可以一起记忆

索引

对于对象,通过属性名索引

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

对于数组,通过数值索引

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

type Person = NonNullable<typeof MyArray[number]["age1"]>; // type Person = number

索引的方式可以抽离我们想要的类型

条件类型

条件类型有助于描述输入和输出类型之间的关系,操作起来更加灵活

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

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

上面这个是 extends 的条件用法,类似三元表达式语法

重载函数的例子

interface IdLabel {
  id: number;
}
interface NameLabel {
  name: string;
}
 
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";
}

这里进行了三个重载。我们可以改造成下述的形式,将输出定义成一个条件类型,可以避免写复杂的重载函数

type nameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;
function createLabel<T extends number | number>(idOrName: T): NameOrId {
    thorw "unimplemented";
}

条件类型约束

更好的限制泛型

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
type Flatten<T> = T extends any[] ? T[number] : T;

在条件类型中进行推断 infer

注意:只有在条件类型 extends 子句中才允许 infer 声明

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

type Num = GetReturnType<() => number>;

type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

主要体现了 infer 的用法,作为辅助类型的方式出现

分配条件类型

当我们向条件类型传入联合类型,如果没有使用 [] 会出现映射联合类型的每个成员类型。

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

type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
type StrArrOrNumArr2 = ToArray2<string | number>; // (string | number)[]

映射类型

当我们想要将另一个类型的属性都拿过来的时候,可以用到映射的概念。keyof 拿出所有类型,in 表示在这些类型中

type Horse = {};

type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};

const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};

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

映射修饰符

readonly? 可以在映射的过程中使用。用 - + 表示减少或添加,默认 +

type CreateMutable<Type> = {
  -readonly [Property in keyof Type]?: Type[Property];
};

type LockedAccount = {
  readonly id: string;
  readonly name: string;
};

type UnlockedAccount = CreateMutable<LockedAccount>;
// type UnlockedAccount = {
//   id?: string | undefined;
//   name?: string | undefined;
// }

通过 as 来重新设置键名

as 可以将键名设置成我们想要的形式,或者通过 never 过滤

联合模式

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;
// }

这个很好用,也很好玩

模板字符串

类似 js 中的模板字符串语法。遇到联合类型会算出每种组合方式

type World = "world";

type Greeting = `hello ${World}`;
// type Greeting = "hello world"

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

type Lang = "en" | "ja" | "pt";

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// 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 PropEventSource<Type> = {
  on(
    eventName: `${string & keyof Type}Changed`,
    callback: (newValue: any) => void
  ): void;
};

declare function makeWatchedObject<Type>(
  obj: Type
): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

person.on("firstNameChanged", (newValue: any) => {
  console.log(`firstName was changed to ${newValue}!`);
});

额,好像没啥不好理解的

使用模板文字进行推理

用模板字符串中的变量来做泛型的类型推断

内在字符串操作类型

intrinsic 是一个关键字,是一种编译

intrinsic 关键字只能用于声明编译器提供的内部类型。

Uppercase<StringType>

将字符串中的每个字符转换为大写版本

type Greeting = "Hello, world";
type ShoutyGreeting = Uppercase<Greeting>;
// type ShoutyGreeting = "HELLO, WORLD"

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;
// type MainID = "ID-MY_APP"

源码

type Uppercase<S extends string> = intrinsic;

Lowercase<StringType>

将字符串中的每个字符转换为小写版本

type Greeting = "Hello, world";
type QuietGreeting = Lowercase<Greeting>;
// type QuietGreeting = "hello, world"

type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`;
type MainID = ASCIICacheKey<"MY_APP">;
// type MainID = "id-my_app"

源码

type Lowercase<S extends string> = intrinsic;

Capitalize<StringType>

将字符串中的第一个字符转换为等效的大写字符

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// type Greeting = "Hello, world"

源码

type Capitalize<S extends string> = intrinsic;

Uncapitalize<StringType>

将字符串中的第一个字符转换为等效的小写字符

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// type UncomfortableGreeting = "hELLO WORLD"

源码

type Uncapitalize<S extends string> = intrinsic;

小结

类型操作让我们更加灵活的使用类型,优化很多繁琐的声明。对于每个知识点都需要熟记,毕竟存在即合理,肯定有它的妙用所在