TypeScript中的泛型

简介: TypeScript中的泛型

TypeScript中的难点:<泛型>

为什么使用泛型

假如我们有这样一个程序:

class KeyValue {
  constructor(public key: number, public value: string) {}
}
let keyValue = new KeyValue(1, "kevin");

那么如果我们想让 key 传入的是 string 类型呢?

我们可能会想到新建一个类,或者使用联合类型,或者使用 any 类型。

但这不是我们应该做的,新建一个类会写重复的代码。使用联合类型不仅会得不到正确的代码补全,并且当 key 为新类型时,还需要在原有代码上增添新类型。使用 any 类型会让我们失去使用 ts 的意义。

这时候我们应该用到泛型(generic)。

在类中使用泛型

class KeyValue<T> {
  constructor(public key: T, public value: string) {}
}
let keyValue1 = new KeyValue<number>(1, "kevin");
let keyValue2 = new KeyValue<string>("2", "kevin");

当我们使用泛型时,我们应该先声明,声明的方法就是在类名后使用尖括号<>然后在里面声明一个或多个类型变量,当声明多个泛型时使用逗号隔开。这个变量可以是任意字母。

不过我们通常使用大写字母开头,并且喜欢使用单个字母。很多时候你会看到T,实际上这来源于 c++的 temple 模板类。

当我们使用的时候会向上面那样传入类型,实际上我们传入类型时 ts 也会进行自动推断类型。这时候我们没有多写代码,同时我们也得到了安全的类型以及智能的代码补全。

现在我们将这个需要实现可复用的类进行完善:

class KeyValue<K, V> {
  constructor(public key: K, public value: V) {}
}
let keyValue1 = new KeyValue<string, boolean>("2", true);
let keyValue2 = new KeyValue(1, "kevin");

在函数和方法中使用泛型

在函数中使用:

function wrapInArray<T>(value: T) {
  return [value];
}

let numebrs = wrapInArray("1");
let numebrs1 = wrapInArray(1);

在方法中使用:

class ArrayUtils {
  wrapInArray<T>(value: T) {
    return [value];
  }
}

let utils = new ArrayUtils();
let numbers = utils.wrapInArray(1);

接口泛型

interface Result<T> {
  data: T;
}

假如我们想要像下面那样,使用接口描述一个请求的返回值:

interface Result<T> {
  data: T | null;
  error: null | string;
}

function fetch<T>(url: string): Result<T> {
  return { data: null, error: null };
}

我们可以这样写:

interface Result<T> {
  data: T | null;
  error: null | string;
}

function fetch<T>(): Result<T> {
  return { data: null, error: null };
}

interface User {
  username: string;
}

interface Product {
  title: string;
}

let result = fetch<Product>();
result.data?.title;

这时候当我们写result.data时 ts 会根据我们传入的类型来进行代码补全的提示。

比如我们这里传入的 Product 接口,那么这时返回值中的 data 的类型为 Product,一个只有属性名为title的对象,所以会提示我们输入 title。至于为什么是可选属性,因为 data 的属性为 T|null 可能为 null 类型。

限制泛型类型

有时候我们需要在构造函数中使用泛型,现在让我们写一个简单的函数

function echo<T>(value: T): T {
  return value;
}

echo("1");

我们可以发现我们能向 echo 中传入任何类型的值,我们可以使用 extends 来限制它。这样只能传入继承的类型。

就像这样:

function echo<T extends string | number>(value: T): T {
  return value;
}

echo("1");
echo(1);
//echo(true); //报错:类型“boolean”的参数不能赋给类型“string | number”的参数。ts(2345)

我们也可以使泛型继承一个接口:

interface Person {
  name: string;
}

function echo<T extends Person>(value: T): T {
  return value;
}

echo({ name: "kevin" });

我们也可以使用类来限制泛型:

class Person {
  constructor(public name: string) {}
}

class Customer extends Person {}

function echo<T extends Person>(value: T): T {
  return value;
}

echo({ name: "kevin" });
echo(new Person("person"));
echo(new Customer("customer"));

传入的值可以是符合类型的对象,实例对象或者子类的实例对象。

泛型类和继承

现在我们有这么段代码:

假如我们有一个商店类,使用 add 方法添加产品。为了让 object 不被外部调用,我们声明其为私有属性,向开头添加下划线_并且使其数组初始化为空数组。

interface Product {
  name: string;
  price: number;
}

class Store<T> {
  private _object: T[] = [];

  add(obj: T): void {
    this._object.push(obj);
  }
}

接下来我们会通过三种场景来扩展这个类

传递泛型 T

interface Product {
  name: string;
  price: number;
}

class Store<T> {
  private _object: T[] = [];

  add(obj: T): void {
    this._object.push(obj);
  }
}

class CompressibleStore<T> extends Store<T> {
  compress() {}
}

let store = new CompressibleStore<Product>();
store.compress();
store.add({ name: "nice", price: 22 });

我们在继承一个声明了泛型的类时,我们这个类也需要声明泛型,因为在这里T作为的是值,而类型变量T在这里还没有声明,所以我们需要先声明它。

限制泛型类型

我们新声明了一个提供查找方法的类。

我们在这个类中提供了 find 方法,然后想要使用 Store 的_object属性,所以我们将修饰词改为protected。数组的 find 方法如果找到了这个元素会返回元素本身,如果没有找到则返回 undefined,所以我们的 find 方法返回值为T | undefined

interface Product {
  name: string;
  price: number;
}

class Store<T> {
  protected _object: T[] = [];

  add(obj: T): void {
    this._object.push(obj);
  }
}

class CompressibleStore<T> extends Store<T> {
  compress() {}
}

class SearchableStore<T extends { name: string }> extends Store<T> {
  find(name: string): T | undefined {
    return this._object.find((obj) => obj.name === name);
  }
}

new SearchableStore<Product>();
new SearchableStore<{ name: string }>();

混合泛型类型属性

也就是在继承时直接传入泛型

interface Product {
  name: string;
  price: number;
}

class Store<T> {
  protected _object: T[] = [];

  add(obj: T): void {
    this._object.push(obj);
  }
}
// 1、传递泛型T
class CompressibleStore<T> extends Store<T> {
  compress() {}
}
// 2、限制泛型类型
class SearchableStore<T extends { name: string }> extends Store<T> {
  find(name: string): T | undefined {
    return this._object.find((obj) => obj.name === name);
  }
}
// 3、修复泛型类型属性
class ProductStore extends Store<Product> {
  filterByCategory(): Product[] {
    return [];
  }
}

泛型与键操作符

假设我们在 Store 中声明了 find 方法,希望能查找 obj。

我们的 property 为键,类型为 string,value 为值,类型为 unknown,因为我们不知道值为什么类型。返回值为 T(obj 的类型)或者 undefined(没有找到)。

我们让 find 的条件为obj[property] === value这时会报错(元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "unknown"。在类型 "unknown" 上找不到具有类型为 "string" 的参数的索引签名。ts(7053))。

因为在使用obj[property]时 ts 会认为我们在使用索引签名。我们处理的只是 T 类型的实质类型。所以我们应该将property的类型改为keyof T,如果传入 Product,这代表类型为'name' | 'price'。

这时我们传入 property 的值只能为 name 或者 price 了。

interface Product {
  name: string;
  price: number;
}

class Store<T> {
  protected _object: T[] = [];

  add(obj: T): void {
    this._object.push(obj);
  }

  // T is Product
  // keyof T => 'name' | 'price'
  find(property: keyof T, value: unknown): T | undefined {
    return this._object.find((obj) => obj[property] === value);
  }
}
let store = new Store<Product>();
store.add({ name: "kevin", price: 2 });
store.find("name", "kevin");
store.find("price", 2);
// store.find("nonExistingProperty", 2); //类型“"nonExistingProperty"”的参数不能赋给类型“keyof Product”的参数。ts(2345)

类型映射

如果我们现在要添加一个接口,它的属性和上面的 Product 一样,只是属性被修饰为 readonly,我们可能会这样写:

interface Product {
  name: string;
  price: number;
}

interface ReadonlyProduct {
  readonly name: string;
  readonly price: number;
}

但是我们不应该写这么重复的代码,我们应该使用类型映射:

interface Product {
  name: string;
  price: number;
}

type ReadonlyProduct = {
  readonly [K in keyof Product]: Product[K];
};

类型映射应该使用type声明,并且我们通过索引签名和 for in 循环来声明属性和类型。并且在属性前添加 readonly 关键字。

interface Product {
  name: string;
  price: number;
}

type ReadonlyProduct = {
  readonly [K in keyof Product]: Product[K];
};

let product: ReadonlyProduct = {
  name: "k",
  price: 2,
};
// product.name = "kevin"; //无法分配到 "name" ,因为它是只读属性。ts(2540)

如果我们希望其他的接口也是只读的,我们可以使用泛型:

interface Product {
  name: string;
  price: number;
}

type ReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

let product: ReadOnly<Product> = {
  name: "k",
  price: 2,
};
// product.name = "kevin"; //无法分配到 "name" ,因为它是只读属性。ts(2540)

现在我们根据上面的经验,创建一个可选属性的类型映射和可能为 null 类型的类型映射:

type Optional<T> = {
  [K in keyof T]?: T[K];
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

这些类型映射是非常有用的,实际上 ts 内置了这些类型映射(工具类型TypeScript: Documentation - Utility Types (typescriptlang.org)

相关文章
|
6月前
|
JavaScript 前端开发 编译器
TypeScript【泛型1、泛型2、声明合并、命名空间 、模块1、模块2、声明文件简介】(五)-全面详解(学习总结---从入门到深化)
TypeScript【泛型1、泛型2、声明合并、命名空间 、模块1、模块2、声明文件简介】(五)-全面详解(学习总结---从入门到深化)
129 0
|
6月前
|
JavaScript 编译器
TypeScript中泛型在函数和类中的应用
【4月更文挑战第23天】TypeScript的泛型在函数和类中提供了灵活性,允许处理多种数据类型。了解泛型是掌握TypeScript类型系统的关键。
|
2月前
|
JavaScript 编译器
typescript之泛型
typescript之泛型
132 60
|
1月前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
2月前
|
JavaScript 安全
typeScript进阶(14)_泛型和注意事项
TypeScript中的泛型允许创建可重用的代码。泛型可以定义函数、接口、类,支持传递类型参数,实现类型安全。泛型可以用于数组,约束类型参数必须符合特定的接口,也可以在接口和类中使用。泛型类可以包含多个类型参数,甚至在泛型约束中使用类型参数。
23 1
typeScript进阶(14)_泛型和注意事项
|
1月前
|
JavaScript 安全 前端开发
TypeScript :枚举&字符&泛型
本文介绍了 TypeScript 中的泛型、约束、枚举和字符操作的基本用法。通过示例代码展示了如何定义和使用泛型函数、类和接口,以及如何利用 `keyof` 约束类型。此外,还介绍了枚举的定义和使用,包括常量枚举和外部枚举的区别。最后,简要说明了 `?.` 和 `??` 操作符的用途,帮助处理可能为空的属性和提供默认值。
|
3月前
|
JavaScript 安全 算法
TypeScript:一个好泛型的价值
TypeScript:一个好泛型的价值
|
4月前
|
JavaScript 前端开发 程序员
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
81 0
|
5月前
|
JavaScript
TypeScript 泛型类型
TypeScript 泛型类型
|
5月前
|
JavaScript Java 编译器
TypeScript 泛型
TypeScript 泛型