跳至主要內容

泛型

樱桃茶大约 13 分钟

泛型

泛型(Generics)是 TypeScript 提供的一个工具,它允许你在定义函数、接口或类的时候不具体指定它们将操作的数据的类型,而是在使用这些函数、接口或类的时候才确定具体的类型。这种方式提供了更大的灵活性,并且可以用于创建可重用的组件。

  • 基础概念:想象你有一个函数,它可以返回数组中的第一个元素。在 JavaScript 中,不关心元素的具体类型,但在 TypeScript 中,我们优先考虑类型安全。

    function getFirstElement(array: any[]): any {
      return array[0];
    }
    

    上面的函数可以返回任何类型的元素,但这样会失去 TypeScript 类型检查的好处。

  • 使用泛型:泛型允许你创建一个工作于多种类型的组件,同时还能保持类型信息。

    function getFirstElement<T>(array: T[]): T {
      return array[0];
    }
    
    let numbers = [1, 2, 3];
    let firstNumber = getFirstElement(numbers); // 类型为 number
    
    let strings = ["a", "b", "c"];
    let firstString = getFirstElement(strings); // 类型为 string
    

    在上面的例子中,<T>表示一个类型变量,你可以在调用 getFirstElement 函数时指定它为任何类型。TypeScript 会根据传入的参数自动推断 T 的类型。

  • 泛型约束:有时候你需要限制泛型的范围,确保它拥有特定的属性或方法。

    interface Lengthwise {
      length: number;
    }
    
    function logLength<T extends Lengthwise>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    
    logLength({ length: 10 }); // 正常工作
    

    在这个例子中,<T extends Lengthwise> 表示任何 T 必须符合 Lengthwise 接口。这就是泛型约束。

  • 泛型接口和类:泛型也可以被用于接口和类,从而定义通用的结构。

    interface GenericIdentityFn<T> {
      (arg: T): T;
    }
    
    function identity<T>(arg: T): T {
      return arg;
    }
    
    let myIdentity: GenericIdentityFn<number> = identity;
    
    // 类的泛型
    class GenericNumber<T> {
      zeroValue: T;
      add: (x: T, y: T) => T;
    }
    
    let myGenericNumber = new GenericNumber<number>();
    myGenericNumber.zeroValue = 0;
    myGenericNumber.add = function (x, y) {
      return x + y;
    };
    

    这里的 GenericIdentityFn<T> 是一个泛型接口,而 GenericNumber<T> 是一个泛型类。通过这种方式,你可以为不同的类型实例化类或者接口。

总的来说,泛型是 TypeScript 强大的功能之一,它增加了代码的灵活性和复用性,同时仍然保持严格的类型安全。通过学习和使用泛型,你可以编写出既通用又类型安全的代码。

泛型之 Hello World

泛型之 Hello World

  • 泛型是 TypeScript 提供的一种工具,它允许我们在定义函数、接口或类的时候不预先指定具体的类型,而是在使用的时候再指定类型的一种特性。
  • 使用泛型可以创建可重用的组件,一个组件可以支持多种类型的数据。这样做可以保持代码的灵活性和复用性。

例如:

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

let output1 = identity<string>("myString");
let output2 = identity<number>(100);
  • 这里identity函数通过一个类型变量T来捕获用户传入的类型(比如stringnumber),之后再用T作为返回类型。当使用identity函数时,你也同时传入了类型参数,告诉它T即所需的类型。

  • 在上述例子中,我们明确地指定了T应该是string类型(identity<string>),以及number类型(identity<number>),因此对于output1output2变量,TypeScript 知道它们分别是字符串和数字类型。

  • 当然,TypeScript 编译器也可以自动地推论出类型参数(即类型自动推断),所以你完全不必明确地传递类型参数<string><number>

let output1 = identity("myString"); // 类型推断为 string
let output2 = identity(100); // 类型推断为 number
  • 在这个简化的例子中,TypeScript 根据传给identity函数的值自动地确定了T的类型。这就如同 Hello World 级别的泛型使用,是泛型最基本的一个示例。

使用泛型变量

泛型是 TypeScript 提供的一种工具,它允许我们编写可重用代码的组件,同时保持类型的兼容性和安全。使用泛型变量时,我们可以创建能够处理任意类型的组件,而不是限制它们仅处理某一特定类型。这样做的好处是增加了代码的灵活性和可复用性,同时还能保持严格的类型检查。

  • 基本概念:想象你有一个函数,该函数接受一个数组并返回该数组的第一个元素。如果没有泛型,你可能需要为每种可能的数组元素类型编写一个函数。但有了泛型,你可以写一个通用的函数,它可以工作于多种类型。

    function getFirstElement<T>(array: T[]): T {
      return array[0];
    }
    

    在上面的例子中,<T>表示泛型类型。当你调用这个函数时,你可以指定T代表的类型,或者让 TypeScript 自动根据传入的参数推断类型。

  • 在类中使用:泛型也可以在类定义中使用,使得类可以操作不特定的数据类型。

    class DataHolder<T> {
      data: T;
    
      constructor(data: T) {
        this.data = data;
      }
    
      getData(): T {
        return this.data;
      }
    }
    
    let numberHolder = new DataHolder<number>(123);
    let stringHolder = new DataHolder<string>("hello");
    

    这里DataHolder类通过使用泛型<T>,成为了可以存储任何类型数据的通用容器。

  • 泛型约束:在某些情况下,你可能希望限制泛型所能代表的类型范围。通过使用泛型约束,你可以指定泛型继承自某个特定类或者拥有某些特定属性。

    interface Lengthwise {
      length: number;
    }
    
    function loggingIdentity<T extends Lengthwise>(arg: T): T {
      console.log(arg.length); // Now we know it has a .length property, so no more error
      return arg;
    }
    

    在上面的例子中,T extends Lengthwise表示T必须符合Lengthwise接口,即必须包含length属性。这样在函数内部就可以安全地访问arg.length了。

使用泛型允许你编写灵活且可重用的代码库,同时保持类型安全。理解并熟练运用泛型将大大提升你使用 TypeScript 的效率和代码质量。

泛型类型

泛型类型是 TypeScript 中的一个高级特性,它允许你定义一个类型或方法,这个类型或方法可以适用于多种数据类型。使用泛型可以创建可重用的组件,这些组件可以支持多种类型的数据。

  • 泛型函数类型:

    • 泛型也可以用在函数类型的定义上。比如说,我们有一个泛型接口定义一个函数:
      interface GenericIdentityFn<T> {
        (arg: T): T;
      }
      
    • 这个接口定义了一个函数,这个函数接收一个参数 arg,这个参数和函数返回值都是同一类型 T
  • 使用泛型类型:

    • 当我们要实现这个接口时,我们会定义一个具体的函数,并且指定 T 的具体类型:

      function identity<T>(arg: T): T {
        return arg;
      }
      
      let myIdentity: GenericIdentityFn<number> = identity;
      
    • 这里我们定义了 myIdentity 函数,它的类型是 GenericIdentityFn<number>,意味着实参和返回值类型都应该是 number 类型。

  • 泛型类类型:

    • 泛型不仅可以用于定义接口或者函数,还可以用来定义类:

      class GenericNumber<T> {
        zeroValue: T;
        add: (x: T, y: T) => T;
      }
      
      let myGenericNumber = new GenericNumber<number>();
      myGenericNumber.zeroValue = 0;
      myGenericNumber.add = function (x, y) {
        return x + y;
      };
      
    • 这个类 GenericNumber<T> 有一个成员 zeroValue 和一个方法 add,它们的类型都依赖于 T。当我们创建一个新的 GenericNumber<number> 的实例时,zeroValueadd 的类型都被确定为 number

泛型类型使得你能够编写灵活、可复用的代码库,你只需要定义一次方法或组件,然后就可以用不同的类型去使用它,而不需要每一种类型都重写方法或组件。这样既增强了代码的可维护性,也增加了程序的灵活性。

泛型类

泛型类是一种组件或类,它可以适用于多种数据类型的同时保持一定的类型安全。在 TypeScript 中,泛型类的定义与普通类非常相似,但它们会带有一个或多个类型变量。

  • 泛型类的定义:使用尖括号<>来指定泛型参数。例如:

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

    在这个例子中,GenericNumber类有一个名为T的泛型类型参数。

  • 实例化泛型类:创建类的实例时,需要指定泛型类型。例如:

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

    这里创建了一个GenericNumber的实例,用number作为其类型参数,因此zeroValue属性和add方法都将处理数字类型的值。

  • 泛型类与不同类型一起使用:可以使用不同的类型参数来实例化泛型类,使得类可以以类型安全的方式工作于多种数据类型。例如:

    let myGenericString = new GenericNumber<string>();
    myGenericString.zeroValue = "";
    myGenericString.add = function (x, y) {
      return x + y;
    };
    

    在这个例子中,GenericNumber被实例化为接受字符串类型的参数,从而zeroValueadd现在适用于字符串操作。

  • 类的静态成员不能使用类的泛型类型参数:如果类具有静态成员,则这些成员不能访问类的泛型类型参数。每个实例化的类型对于静态成员来说是不可见的。

使用泛型类可以编写出更加灵活且可重用的代码组件,因为它们允许你在保持类型安全的前提下传入不同的类型。

泛型约束

泛型约束(Generic Constraints)是 TypeScript 中用来确保泛型类型符合特定结构或者包含必需的属性的一种机制。在使用泛型时,你可能想要限制泛型能够接受哪些类型的数据。这就是通过泛型约束实现的。

  • 基本概念:

    • 泛型给予我们创建可复用组件的能力,这些组件可以支持多种类型而不丢失其类型信息。
    • 在有些情况下,你可能需要对这些类型进行一些限制,以保证它们具有某些共同的功能或属性。
  • 例子:

    • 假设你有一个函数,你希望这个函数能够处理任何带有 .length 属性的类型(如数组,字符串等)。
    • 不使用泛型约束,这个函数可能会接收任何类型,但如果传入的类型没有 .length 属性,就会出错。
    • 使用泛型约束,你可以明确指出该函数只接受有 .length 属性的类型。
// 没有泛型约束的函数
function logLength<T>(arg: T): void {
  // 这里可能会因为arg没有length属性而导致编译错误
  console.log(arg.length);
}

// 使用泛型约束
interface Lengthwise {
  length: number;
}

// 现在T被约束了,它必须符合Lengthwise接口
function logLengthWithConstraint<T extends Lengthwise>(arg: T): void {
  // 因为arg一定有length属性,所以下面的代码是安全的
  console.log(arg.length);
}

logLengthWithConstraint([1, 2, 3]); // 输出:3
logLengthWithConstraint("hello"); // 输出:5

// 下面将导致编译错误,因为数字没有length属性
// logLengthWithConstraint(10);
  • 泛型约束的语法:
    • 你通过 extends 关键字添加泛型约束,后面跟着约束条件。
    • 约束条件通常是一个接口或类型别名,它定义了泛型应该满足的属性或方法。
// T extends U的语法表示T必须符合U的形状
function identity<T extends U, U>(arg: T): U {
  return arg;
}
  • 使用泛型约束时的注意事项:
    • 过度使用泛型约束可能会导致你的代码变得过于复杂。
    • 尽量让泛型保持简单,只在必要时添加约束。
    • 当你无法提前知道具体的类型,但是需要确保类型间有一定的关系或兼容性时,泛型约束非常有用。

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

泛型约束中使用类型参数的概念指的是在 TypeScript 中,当你定义一个泛型函数时,可能想限制这个函数可以接受的类型。比如说,你想确保这个类型有某个特定的属性或方法。这时候,你就可以使用泛型约束。

  • 基本的泛型约束通常是用一个接口或者 type 来定义那些必须存在于传入类型的属性或方法。

    interface Lengthwise {
      length: number;
    }
    
    function logLength<T extends Lengthwise>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    

    在这个例子中,T extends Lengthwise表示任何传入logLength函数的类型 T 都必须满足Lengthwise接口,即具有length属性。

  • 泛型约束中还可以使用类型参数来定义另一个类型参数的约束。

    function getProperty<T, K extends keyof T>(obj: T, key: K) {
      return obj[key];
    }
    
    let x = { a: 1, b: 2, c: 3, d: 4 };
    
    getProperty(x, "a"); // 正常运行
    // getProperty(x, "m"); // 错误:Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
    

    在这里,K extends keyof T意味着 K 必须是 T 的键集合(即 T 的所有键的类型)中的一个。这样就能确保getProperty函数不会尝试访问对象上不存在的属性,提供了类型安全。

通过使用泛型约束,可以编写出更加安全、适应性强的代码,因为你可以预先定义并保证某些类型的条件和结构。

在泛型里使用类类型

在 TypeScript 的泛型约束中,使用类类型允许你指定一个泛型不仅要符合某个接口,而且还必须是某个特定类或其子类的实例。这样就可以确保泛型不只满足结构上的约束,同时也可以享有类本身提供的方法和属性。

  • 泛型约束基础:通常我们使用泛型来创建可重用的组件,但是有时候我们需要对泛型进行一些限制,以确保它们具有我们需要的特定功能。这时,我们会使用extends关键字来约束泛型。
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(item: T): void {
  console.log(item.length);
}

在上述代码中,T extends Lengthwise意味着任何使用logLength函数的泛型T必须具有length属性。

  • 在泛型中使用类类型:通过在泛型约束中使用类类型,你可以确保泛型参数是一个特定的类,或者是继承自该类的子类。这使你能够创建出既具有类的特性又具备泛型灵活性的函数或类。
class Animal {
  numLegs: number;

  constructor(numLegs: number) {
    this.numLegs = numLegs;
  }

  walk() {
    console.log(`Walking on ${this.numLegs} legs.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function createInstance<A extends Animal>(C: new () => A): A {
  return new C();
}

在上面的代码中,createInstance函数要求传入的C参数必须是一个类,并且这个类必须有一个无参数的构造函数,并且返回值的类型为A,其中A必须是Animal的实例或者派生自Animal类的类的实例。

  • 使用createInstance
const myDog = createInstance(Dog);
myDog.walk(); // 正常工作,因为Dog继承自Animal,拥有walk方法
myDog.bark(); // 正常工作,Dog类有bark方法

在上述例子中,myDog是通过createInstance函数创建的Dog实例,因为Dog类符合createInstance函数中对于泛型参数A的约束(即AAnimal的实例或其子类)。

这种技术允许你编写出既通用又安全的代码,因为你可以利用泛型来写出适用于多种类型的代码,同时又能保证这些类型具有你所需的特定属性或方法。