[TypeScript学习笔记-07]根据现有类型创建新类型的N种方式

TypeScript 的类型系统非常强大,因为它允许用其他类型来表示类型。这种理念最简单的形式就是泛型。此外,我们还可以使用各种各样的类型运算符。也可以用已有的值来表示类型。通过组合各种类型运算符,我们可以以简洁、易于维护的方式表达复杂的操作和值。

1. 泛型(Generics)

软件工程的一个重要组成部分是构建组件,这些组件不仅要拥有定义明确且一致的API,还要具备可重用性 。能够处理当前数据以及未来数据 的组件,将为构建大型软件系统提供最大的灵活性。在 C# 和 Java 等语言中,创建可重用组件的主要工具之一是泛型,即能够创建可处理多种类型而非单一类型的组件。这使得用户可以使用这些组件并应用他们自己的类型。

1.1 泛型函数:

  • 常规表示:
typescript 复制代码
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: <Type>(arg: Type) => Type = identity;
  • 基于interface的定义:
typescript 复制代码
interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn = identity;

或者:

typescript 复制代码
interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;

1.2 泛型类

  • 常规定义:
typescript 复制代码
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;
};

: add 是函数类型属性,并非常规方法,所以类本身不需要提供实现。

  • 我们也可以使用字符串,甚至更复杂的对象。
typescript 复制代码
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};
 
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

1.3 泛型约束

  • 常规定义
typescript 复制代码
interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); 
  return arg;
}
  • 你可以声明一个受另一个类型参数约束的类型参数。例如,这里我们想根据对象名称获取其属性。为了确保不会意外获取对象上不存在的属性,我们将在两个类型之间添加约束:
typescript 复制代码
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a"); // 正确
getProperty(x, "m"); // 错误
  • 在 TypeScript 中使用泛型创建工厂时,必须通过构造函数来引用类类型。例如:
typescript 复制代码
function create<Type>(c: { new (): Type }): Type {
  return new c();
}
  • 设置默认泛型参数
typescript 复制代码
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[]>

1.4 协变与逆变(covariance & covariance)

  • 协变(covariance):我们可以使用 Producer<Cat> 来代替预期的 Producer<Animal>,因为猫属于动物。这种关系称为协方差:从 Producer<T>Producer<U> 的关系与从 T 到 U 的关系相同。
typescript 复制代码
interface Producer<out T> {
  make(): T;
}
  • 逆变(covariance):如果可以在预期是 Consumer<Cat> 的地方使用 Consumer<Animal>,因为任何能够接受 Animal 的函数也必然能够接受 Cat。这种关系称为逆变:从 Consumer<T>Consumer<U> 的关系与从 UT 的关系相同。注意与协变相比,方向相反!这就是为什么逆变会自我抵消,而协变不会。
typescript 复制代码
interface Consumer<in T> {
  consume: (arg: T) => void;
}

2. Keyof 操作符

keyof 运算符接受一个对象类型,并生成其键的字符串或数值字面量联合

typescript 复制代码
type Point = { x: number; y: number };
type P = keyof Point;
  • 如果类型具有字符串或数字索引签名,keyof 将返回这些类型:
typescript 复制代码
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

3. Typeof 操作符

TypeScript 添加了 typeof 运算符,您可以在类型上下文中使用它来引用变量或属性的类型 (不同于JavaScript的typeof 操作符)。

typescript 复制代码
let s = "hello";
let n: typeof s; // let n: string
  • 可以使用 typeof 来方便地表达许多模式。例如,我们先来看预定义的类型 ReturnType。它接受一个函数类型并生成其返回类型:
typescript 复制代码
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; //type K = boolean
  • ReturnType 中的 TFunction 是函数类型, 不是函数本身
typescript 复制代码
function f() {
  return { x: 10, y: 3 };
}
type P = ReturnType<f>; // 错误

应该换成

typescript 复制代码
function f() {
  return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;

// type P = {
//     x: number;
//     y: number;
// }

4. 利用索引访问类型

  • 我们可以使用索引访问类型来查找另一种类型的某个属性的类型:
typescript 复制代码
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = number
  • 索引类型本身也是一种类型,因此我们可以完全使用联合类型、keyof 类型或其他类型:
typescript 复制代码
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
  • 可以使用 number 来获取数组元素的类型。我们可以将其与 typeof 结合使用,以便方便地获取数组字面量的元素类型:
typescript 复制代码
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
// Or
type Age2 = Person["age"]; // type Age2 = number
  • 索引时只能使用类型,这意味着不能使用 const 来引用变量:
typescript 复制代码
const key = "age";
type Age = Person[key]; // 错误

如下是正确的:

typescript 复制代码
type key = "age";
type Age = Person[key];

type Age2 = Person["age"] 中的"age" 是类型

5. 条件类型(Conditional Types)

5.1 描述输入和输出类型之间的关系。

typescript 复制代码
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
  • 条件类型的形式与 JavaScript 中的条件表达式(条件 ? trueExpression : falseExpression)类似:
typescript 复制代码
SomeType extends OtherType ? TrueType : FalseType;
  • 条件类型的强大之处在于它们与泛型结合使用。
typescript 复制代码
interface IdLabel {
  id: number 
}
interface NameLabel {
  name: string
}

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;
  • 我们可以利用这种条件类型,将重载函数简化为一个没有重载的单一函数。
typescript 复制代码
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
 
let a = createLabel("typescript");  //let a: NameLabel 
let b = createLabel(2.8);   //let b: IdLabel 
let c = createLabel(Math.random() ? "hello" : 42); // let c: NameLabel | IdLabel
  • 更复杂的例子:
typescript 复制代码
type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>; // type EmailMessageContents = string
typescript 复制代码
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
typescript 复制代码
type Flatten<T> = T extends any[] ? T[number] : T;
 
type Str = Flatten<string[]>;   // type Str = string
type Num = Flatten<number>;     //type Num = number

5.2 在条件类型中进行推断

条件类型为我们提供了一种方法,可以使用 infer 关键字从我们在 true 分支中比较的类型进行推断。

typescript 复制代码
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

: 我们使用 infer 关键字以声明的方式引入了一个名为 Item 的新泛型类型变量,而不是在 true 分支中指定如何检索 Type 的元素类型。这使我们无需考虑如何深入挖掘和探测我们感兴趣的类型的结构。

  • 我们可以使用 infer 关键字编写一些有用的辅助类型别名:
typescript 复制代码
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[]
  • 当从具有多个调用签名的类型(例如重载函数 的类型)进行类型推断时,推断基于最后一个签名(这通常是最宽松的兜底情况)。无法基于参数类型列表执行重载解析。
typescript 复制代码
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>; // type T1 = string | number

5.3 可分配的条件类型(Distributive Conditional Types)

当条件类型作用于泛型类型时,如果给定的是一个联合类型,则它们就具有分配律。

typescript 复制代码
type ToArray<Type> = Type extends any ? Type[] : never; 
type StrArrOrNumArr = ToArray<string | number>; 
// type StrArrOrNumArr = string[] | number[]
  • 通常情况下,分配特性是理想的行为。为了避免 这种特性,你可以用方括号将 extends 关键字的每一侧括起来。
typescript 复制代码
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; 
type ArrOfStrOrNum = ToArrayNonDist<string | number>; 
// type ArrOfStrOrNum = (string | number)[]

6. 映射类型(Mapped Types)

Mapped Types 是 TypeScript 提供的一种强大工具,用来基于已有类型生成新类型。它的核心思想是:对某个类型的属性集合进行遍历,并在遍历过程中应用规则来生成新的属性类型。

  • 映射类型建立在索引签名语法的基础上,索引签名用于声明尚未预先声明的属性类型:
typescript 复制代码
type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};
 
const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};
  • 映射类型是一种通用类型,它使用 PropertyKey 的联合(通常通过 keyof 创建)来遍历键以创建类型:
typescript 复制代码
type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

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

keyof 操作符返回的是字面量类型的联合, Property 仅仅是一个占位符,可以使用任意字符代替。

6.1 映射修饰符

  • 映射过程中还可以应用两个额外的修饰符:readonly?,它们分别影响可变性和可选性。您可以通过在前缀中添加 -+移除添加 这些修饰符。如果您不添加前缀,则默认为 +
typescript 复制代码
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property]; // 移除 readonly
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = CreateMutable<LockedAccount>;
           
// type UnlockedAccount = {
//     id: string;
//     name: string;
// }
typescript 复制代码
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 子句重新映射映射类型中的键:

typescript 复制代码
type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}
  • 可以利用模板字面量类型等功能,根据已有的属性名称创建新的属性名称:
typescript 复制代码
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;
// }

string & Property 提出非字符串类型的字面量。

  • 您可以通过条件类型生成 never 来过滤掉某些键:
typescript 复制代码
// 剔除 "kind" 字面量类型
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
           
// type KindlessCircle = {
//     radius: number;
// }
  • 你可以对任意联合进行映射,不仅限于字符串、数字或符号的联合,而是任何类型的联合:
typescript 复制代码
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;
// }

: E是针对联合成员的遍历,所以E在这里代表的类型就是SquareEventCircleEventE["kind"]为kind属性的字面量类型。

  • 映射类型可以很好地与此类型操作部分中的其他功能配合使用:
typescript 复制代码
type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
 
type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
};
 
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
                 
// type ObjectsNeedingGDPRDeletion = {
//     id: false;
//     name: true;
// }

7. 模板字面类型(Template Literal Types)

模板字面量类型基于字符串字面量类型 构建,并且可以通过联合 扩展为多个字符串 。它们的语法与 JavaScript 中的模板字面量字符串相同,但用于类型位置。当与具体的字面量类型一起使用时,模板字面量会通过连接内容生成一个新的字符串字面量类型。

typescript 复制代码
type World = "world"; 
type Greeting = `hello ${World}`; //type Greeting = "hello world"
  • 联合用于插值位置时,其类型是每个联合成员可以表示的所有可能的字符串字面量的集合:
typescript 复制代码
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"
  • 对于模板字面量中的每个插值位置,将并集进行交叉相乘
typescript 复制代码
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.1 类型中的字符串联合

typescript 复制代码
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", () => {}); // 正确 
person.on("firstName", () => {}); // 错误
person.on("frstNameChanged", () => {}); // 错误(拼写错误)

7.2 使用模板字面量进行推断

typescript 复制代码
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 字符串操作类型

为了便于字符串操作,TypeScript 包含了一组可用于字符串操作的类型。这些类型内置于编译器中以提高性能,因此在 TypeScript 自带的 .d.ts 文件中找不到。

  • Uppercase
typescript 复制代码
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"
  • Lowercase
typescript 复制代码
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"
  • Capitalize
typescript 复制代码
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;        
// type Greeting = "Hello, world"
  • Uncapitalize
typescript 复制代码
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;              
// type UncomfortableGreeting = "hELLO WORLD"
相关推荐
东风破_6 小时前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
晓说前端8 小时前
TypeScript 核心语法应用 —— Vue 3 中的使用(下)
前端·javascript·typescript·类型系统
东风破_10 小时前
(拼多多考题)TypeScript 工具类型:Pick、Omit、Partial、Required
typescript
rimydu1974art12 小时前
opencheck解读:拆解 OpenCheck 的歧视性条款识别与澄清问询机制
人工智能·typescript
触底反弹1 天前
🔥 从「为什么」到「怎么用」:TypeScript 类型约束与泛型完全指南
后端·面试·typescript
Asize1 天前
2 道大厂面试题:TS 工具类型我懂了,CSS 3 列布局把我问住了
前端·css·typescript
电脑玩家柒柒2 天前
DeepSeek Harness 生态观察:插件一夜爆火、标准预设与桌面 Studio,Agent 框架的安卓时刻来了吗
typescript
爱酱丶2 天前
VS Code 快速生成Vue3 + TypeScript + Setup 基础空模板
前端·javascript·typescript
sugar__salt2 天前
三列布局与 TypeScript 工具类型 Pick / Omit / Partial 详解
前端·javascript·typescript