Typescript 类型操作

71 阅读10分钟

根据类型创建类型

TypeScript的类型系统非常强大,因为它允许用其他类型来表示类型。

这种思想的最简单形式是泛型。此外,我们有各种各样的类型操作符可供使用。也可以用我们已有的值来表示类型。

通过组合各种类型操作符,我们可以以简洁、可维护的方式表达复杂的操作和值。在本节中,我们将介绍用现有类型或值表示新类型的方法。

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

1. 泛型

1.1 为什么使用泛型?

为了增加类型的可复用性。

1.2 Hello World

我们写一个 identity 函数,它接受什么类型的参数,就有什么类型的返回值

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

我们可以这样使用

let output = identity<string>("myString");

也可以这样

let output = identity("myString");

第二种方式更常见,这里利用了类型参数推断。编译器会自动根据我们传入的参数将 T 推断为 string

1.3 泛型类型

只要类型变量的数量和类型变量的使用方式一致,我们也可以为类型中的泛型类型参数使用不同的名称。

function identity<Type>(arg: Type): Type {
    return arg;
}

let myIdentity: <Input>(arg: Input) => Input = identity;

我们还可以将泛型类型写成对象文字类型的调用签名

function identity<Type>(arg: Type): Type {
    return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;

这将引导我们编写第一个泛型接口。让我们从上一个示例中获取对象字面量,并将其移动到一个接口中。

interface GenericIdentityFn<Type> {
    (arg: Type): Type;
}

function identity<Type>(arg: Type): Type {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

1.4 泛型类

泛型类具有与泛型接口相似的形状。泛型类在类名后面的尖括号(<>)中有一个泛型类型参数列表。

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

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
    return x + y;
};

使用

let stringNumeric = new GenericNumber<string>();

stringNumeric.zeroValue = "";

stringNumeric.add = function (x, y) {
    return x + y;
};


console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

类的类型有两个方面: 静态方面和实例方面。泛型类只在其实例端泛型,而不是在其静态端泛型,因此在处理类时,静态成员不能使用类的类型参数。

1.5 泛型限制

假如我们希望在 arg 里取 .length 属性

function loggingIdentity<Type>(arg: Type): Type {
    console.log(arg.length);
    return arg;
}

我们需要限制 arg 使它必须有 length 属性

interface Lengthwise {
    length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
    console.log(arg.length);
    return arg;
}

使用

loggingIdentity({ length: 10, value: 3 });

1.6 在泛型中用类作为类型

在TypeScript中使用泛型创建工厂时,有必要通过构造函数引用类类型。例如

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

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

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  numLegs = 6;
  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;

该模式用于支持 mixins 设计模式。

1.7 泛型参数默认值

declare function create<T extends HTMLElement = HTMLDivElement, U extends HTMLElement[] = T[]>(
  element?: T,
  children?: U
): Container<T, U>;
 
const div = create();
      
const div: Container<HTMLDivElement, HTMLDivElement[]>
 
const p = create(new HTMLParagraphElement());
     
const p: Container<HTMLParagraphElement, HTMLParagraphElement[]>

泛型参数默认值遵循以下规则

  • 如果类型参数有默认值,则认为它是可选的。
  • 必须的类型参数不能放在可选类型参数后面
  • 类型参数的默认类型必须满足类型参数的约束(如果存在)。

2. keyof 类型操作符

keyof操作符接受一个对象类型,并产生其键的字符串或数字字面值联合。下面的类型 P 与类型 P = "x" | "y 相同

type Point = { x: number; y: number };
type P = keyof Point;

如果类型具有字符串或数字索引签名,则keyof将返回这些类型

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number
 
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
// type M = string | number

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

3. typeof 类型操作符

JavaScript 已经有了一个typeof操作符,可以在表达式上下文中使用

// Prints "string"

console.log(typeof "Hello world");

TypeScript添加了一个typeof操作符,你可以在类型上下文中使用它来引用变量或属性的类型

let s = "hello";
let n: typeof s;

// let n: string

这对于基本类型不是很有用,但是结合其他类型操作符,您可以使用 typeof 方便地表达许多模式。例如,让我们从查看预定义类型 ReturnType<T> 开始。它接受一个函数类型并产生它的返回类型

type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
    
// type K = boolean

如果我们尝试在函数名上使用ReturnType,就会看到一个这样的错误

function f() {
  return { x: 10, y: 3 };
}
type P = ReturnType<f>;
// 'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?

记住,值和类型不是一回事。要引用值 f 的类型,使用 typeof

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

4. 索引访问类型

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

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

索引类型本身就是一个类型,因此我们可以完全使用联合、keyof或其他类型

type I1 = Person["age" | "name"];
     
// type I1 = string | number
 
type I2 = Person[keyof Person];
     
// type I2 = string | number | boolean
 
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
     
// type I3 = string | boolean

如果尝试索引一个不存在的属性,甚至会看到一个错误

type I1 = Person["alve"];
// Property 'alve' does not exist on type 'Person'.

另一个使用任意类型进行索引的例子是使用number来获取数组元素的类型。我们可以将它与typeof结合起来,方便地捕获数组字面量的元素类型

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;
}
*/
type Age = typeof MyArray[number]["age"];
     
// type Age = number

type Age2 = Person["age"];
      
// type Age2 = number

您只能在索引时使用类型,这意味着您不能使用 const 来进行变量引用

const key = "age";
type Age = Person[key];
// Type 'key' cannot be used as an index type.
// 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?

但是,您可以使用类型别名进行类似风格的重构

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

5. 条件类型

输入决定输出,是大多数程序语言都有的。ts 也不例外。

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

条件类型就像条件表达式一样

SomeType extends OtherType ? TrueType : FalseType;

当 extends 左边的类型可以赋值给右边的类型,那么得到的结果就是 ? 后面的分支, 否则就是 : 后面的分支。

条件类型,看起来没啥用,一眼就能看出结果。但是如果与泛型结合起来,就会很有用。

看个例子:

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

这里的函数重载,其实就是表达,当传入的参数是 number 类型时,返回的结果是 IdLabel 类型, 传入的是 string 类型时,返回的结果是 NameLabel 类型,分别构成重载的第一、二条,最后一条囊括所有情况。

我们可以用条件类型来写

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

5.1 条件类型可以用于类型约束

例子1:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
              
// type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
             
// type DogMessageContents = never

例子2:

type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>;
     
// type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;
     
// type Num = number

5.2 在条件类型中作类型推断

在 5.1 中,我们使用索引,获取数组项的类型。这里我们使用关键字 infer 来获取。

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

在这里我们引入 Item 作为泛型参数变量

我们还可以用 infer 关键字来获取函数的返回值类型

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
 
type Num = GetReturnType<() => number>;
     
// type Num = number
 
type Str = GetReturnType<(x: string) => string>;
     
// type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
      
// type Bools = boolean[]

5.3 条件类型中的分配规则

看下面的例子

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

通常,这种分配规则是预期的。若想要避免这种分配规则,你可以在 extends 关键字两边的加上中括号 []

像这样:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
          
// type ArrOfStrOrNum = (string | number)[]

6. 映射类型

映射类型以索引签名的语法为基础,索引签名用于声明未提前声明的属性的类型


type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};
 
const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};

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

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

在本例中,OptionsFlags将从type类型中获取所有属性,并将其值更改为布尔值。

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};
 
type FeatureOptions = OptionsFlags<Features>;
           
/*
type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
}
*/

6.1 映射修饰符

在映射期间可以应用另外两个修饰符: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;
    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;
}
*/   

6.2 使用 as 做重影射

在TypeScript 4.1及以后的版本中,你可以在映射类型中使用 as 子句重新映射 key

type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}

您可以利用模板文字类型等特性从先前的属性名创建新的属性名

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

您可以通过条件类型生成 never 来过滤掉 key

// 去除 'kind' 字段
type RemoveKindField<T> = {
    [P in keyof T as P extends 'kind' ? never : P]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
/*
type KindlessCircle = {
    radius: number;
}
*/           

7. 模板字符串类型

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

就像 js 语法一样

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

7.1 字符串字面量联合类型

当在插入位置使用字符串字面量联合类型时,该类型是可以由每个联合成员表示的所有可能的字符串字面值的集合

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 AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_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"

7.2 进阶使用模板字面量

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
/// Create a "watched object" with an `on` method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

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

此时回调接收的参数类型还是 any, 结合泛型可以对其进行约束

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}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", newName => {                             
// (parameter) newName: string
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {                   
// (parameter) newAge: number
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

7.3 内置的字符串操作类型

  1. 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 MyCapitalize<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S
  1. 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"
  1. Capitalize<StringType>

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

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
        
// type Greeting = "Hello, world"
  1. Uncapitalize<StringType>

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

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