ts保姆级学习指南

avatar
作者
猴君
阅读量:0

什么是 TypeScript?

TypeScript,简称 ts,是 JavaScript 的超集,而且它最大的特点之一就是引入了静态类型支持。这意味着开发者可以在 TypeScript 中定义变量、函数参数等的类型,编译器会在编译时进行类型检查,从而帮助开发者捕捉到一些在 JavaScript 中难以发现的错误,比如类型不匹配、未定义的属性等问题。这种类型检查能够提升代码的可靠性和可维护性,尤其是在大型项目中特别有用。

另外,TypeScript 还支持最新的 ECMAScript 标准,因此可以使用 JavaScript 的最新特性,同时还能将 TypeScript 编译成兼容各种 JavaScript 版本的代码,以确保跨平台的兼容性。

总之,TypeScript 不仅仅是 JavaScript 的一个扩展,它通过引入静态类型检查和其他语言特性,使得开发大型应用程序更加高效、可靠。

那么 ts 和 js 有什么区别呢?

TypeScript(简称为TS)和JavaScript(简称为JS)之间主要有几个区别:

类型系统:

  • TypeScript 引入了静态类型系统,允许开发者在声明变量、函数参数、返回值等时指定类型。编译器会进行类型检查,检测类型不匹配的错误。
  • JavaScript 是动态类型语言,变量类型在运行时确定,不需要显式声明类型。

编译:

  • TypeScript 需要先经过编译成 JavaScript 才能在浏览器或者其他 JavaScript 运行环境中执行。
  • JavaScript 是一种解释型语言,可以直接在浏览器或其他环境中执行。

生态系统:

  • JavaScript 生态系统非常广泛,有大量的库、框架和工具支持,适用于各种开发场景。
  • TypeScript 生态系统在不断扩展中,许多流行的 JavaScript 库和框架已经或者正在增加对 TypeScript 的支持。

错误检测:

  • TypeScript 在编译阶段能够检测出许多类型相关的错误,例如变量类型不匹配、未定义的属性访问等,帮助提前发现和修复潜在的问题。
  • JavaScript 的错误通常是运行时才能被发现的,因为它没有静态类型检查。

语言特性:

  • TypeScript 支持最新的 ECMAScript 标准,可以使用 JavaScript 新增的特性,并且还引入了一些自身的语言特性(如接口、枚举、泛型等),增强了代码的表达能力和可读性。
  • JavaScript 语法较为灵活,但在处理复杂的数据结构和大规模代码时,可能会因为缺乏类型检查而带来一些维护上的挑战。

如何安装与使用ts

1、安装node.js(可参考这篇文章

2、下载一个全局的包

npm i  -g  typescript 或者 yarn  global add  typescript  

打开cmd 输入命令 tsc -v 查看包是否下载成功,当 cmd 输入指令后出现版本号,就说明下载成功啦

3、打开 vscode, 新建一个 .ts文件。如 hello.ts

4、在vscode商店安装Code Runner(具体用法)、Error Lens(自动显示代码错误提示)插件,方便后续学习

5、书写代码

console.log("Hello, world!");

 运行后成功在终端打印 Hello, world! 


TypeScript 极速学习

类型声明

类型声明(Type Annotations)是 TypeScript 中的一种语法,用于显式地指定变量、函数参数、函数返回值等的类型。通过类型声明,开发者可以告诉 TypeScript 编译器在编译时进行静态类型检查,从而捕获可能的类型错误。

在 TypeScript 中,类型声明通常使用 : 符号来指定,

变量声明

let num: number = 5; let message: string = "Hello, TypeScript!"; let isActive: boolean = true; 

函数参数和返回值声明

function add(x: number, y: number): number {     return x + y; } 

上面的例子中,add 函数接受两个参数 x 和 y,它们都是 number 类型,并且函数的返回值也声明为 number 类型。

对象和数组声明

let person: { name: string, age: number } = {     name: "Alice",     age: 30 };  let numbers: number[] = [1, 2, 3, 4, 5]; 

person 对象中的 name 是 string 类型,age 是 number 类型。numbers 数组中的元素都是 number 类型。 

类型推断

TypeScript 还支持类型推断,即编译器可以根据变量的初始化值自动推断出其类型,例如:

let someNumber = 10; // TypeScript 自动推断 someNumber 是 number 类型 

类型总览

注意点: JS 中的三个构造函数: Number 、 String 、 Boolean ,他们只⽤于包装对象,正
常开发时,很少去使⽤他们,在TS 中也是同理。

常⽤类型

字⾯量

在 TypeScript 中,字面量是指表示固定值的直接量,例如直接写入的数值、字符串或布尔值。在类型系统中,字面量可以用来精确地指定变量、函数参数或对象属性的取值范围,从而增强类型的精确度和可读性。

数字字面量

let num: 5 = 5; let anotherNum: 10 | 20 | 30 = 20; // 表示变量可以取 10、20 或 30 中的一个值 let anotherNum2: 10 | 20 = 30; // 表示变量可以取 10 或 20 中的一个值,但 30 不能赋值给这个变量 

字符串字面量:

let str: "hello" = "hello"; type Direction = "up" | "down" | "left" | "right"; // 定义一个字符串字面量联合类型 

布尔字面量:

let isOpen: true = true; 

对象字面量

let person: { name: "Alice", age: 30 } = { name: "Alice", age: 30 }; 

any

any 类型在 TypeScript 中的含义是允许变量可以是任意类型。将一个变量声明为 any 类型后,TypeScript 编译器会跳过对该变量的类型检查,这意味着:

  • 类型检查放宽:使用 any 类型后,TypeScript 不会对该变量的使用进行类型检查。这使得你可以对这个变量执行任何操作,即使这些操作在 TypeScript 中可能不安全或不符合预期的类型约束。
  • 运行时类型转换:any 类型的变量可以随意赋予任何类型的值,不会触发类型错误或警告。这种灵活性在一些特定情况下可能是必要的,比如处理来自动态数据源的数据。
  • 失去类型安全性:由于放弃了类型检查,使用 any 类型的变量可能导致运行时错误,因为 TypeScript 不会在编译阶段检测出潜在的类型不匹配问题。

虽然 any 类型在某些情况下提供了灵活性,但过度使用它会导致代码失去 TypeScript 提供的静态类型检查的主要好处。推荐在能够确定类型的情况下,尽量避免使用 any 类型,

//明确的表示a的类型是any —— 显式的any let a: any //以下对a的赋值,均⽆警告 a = 100 a = '你好' a = false //没有明确的表示b的类型是any,但TS主动推断了出来 —— 隐式的any let b //以下对b的赋值,均⽆警告 b = 100 b = '你好' b = false /* 注意点:any类型的变量,可以赋值给任意类型的变量 */ let a1 let x: string x = a1 // ⽆警告

unknown

unknown 类型确实与 any 类型有相似之处,但在类型安全方面更加严格和精确。

  • 类型安全性:unknown 类型比 any 类型更加类型安全。当一个变量被声明为 unknown 类型时,你不能直接对它进行操作或者将它赋给其他类型的变量,除非进行类型检查或者类型断言。
  • 用途:unknown 类型适用于那些一开始无法确定具体类型,但后续可以进行类型检查并确定类型的情况。这种特性使得 unknown 更适合在运行时进行类型检查的场景,以确保类型安全性。
// 设置a的类型为unknown let a: unknown //以下对a的赋值,均正常 a = 100 a = false a = '你好' // 设置x的数据类型为string let x: string x = a //警告:不能将类型“unknown”分配给类型“string”

若就是想把a 赋值给 x ,可以⽤以下三种写法:

1、类型断言 (as 语法):

使用类型断言可以告诉编译器,你已经进行了类型检查,确定 a 是一个 string 类型。

// 设置a的类型为unknown let a: unknown a = 'hello' let x: string; x = a as string;  // 使用类型断言将 unknown 类型断言为 string 类型 

2、typeof 类型保护:

使用 typeof 运算符进行类型保护,这种方式在你已知 a 的类型可以是 string 的情况下比较适用。

// 设置a的类型为unknown let a: unknown a = 'hello' let x: string; if (typeof a === 'string') {     x = a;  // 在类型检查后直接赋值 }  

3、自定义类型保护函数:

可以定义一个自定义的类型保护函数,以在多个地方重复使用。

// 设置a的类型为unknown let a: unknown a = 'hello' function isString(value: unknown): value is string {     // 判断 value 的类型是否为 'string'     return typeof value === 'string'; }  let x: string; if (isString(a)) {     x = a;  // 使用自定义类型保护函数进行类型检查和赋值 } 

never

never 类型是一个特殊的类型,表示那些永远不应该出现的值的类型。

1、几乎不用 never 直接限制变量:

never 类型通常不会直接用于限制变量,因为它是一个空集合的类型,意味着它不能包含任何值。因此,将一个变量显式声明为 never 意味着你永远不可能给这个变量赋值,这在大多数情况下是没有意义的。

/* 指定a的类型为never,那就意味着a以后不能存任何的数据了 */ let a: never // 以下对a的所有赋值都会有警告 a = 1 a = true a = undefined a = null

2、never 一般是 TypeScript 主动推断出来的:

never 类型通常是 TypeScript 在类型推断中推断出来的结果,特别是在处理函数或者条件分支时。例如,一个函数如果抛出异常或者总是抛出错误,它的返回类型就会被推断为 never。

function throwError(message: string): never {     throw new Error(message); } 

在这个例子中,throwError 函数的返回类型是 never,因为它永远不会正常返回,而是会抛出一个错误。 

3、never 用于限制函数的返回值:

never 类型可以用来明确指定函数永远不会返回任何值。这在一些特殊的情况下很有用,比如处理不可到达的终止点。

function infiniteLoop(): never {     while (true) {         // 无限循环,不返回任何值     } } 

另外,never 类型还可以用于类型守卫中,帮助编译器理解一些条件下的类型信息,例如:

function isNever(value: any): value is never {     throw new Error("This function should never be called."); } 

总结来说,never 类型在 TypeScript 中主要用于表示不可能存在的值或者永远不会发生的情况,例如函数抛出异常、无限循环等。它通常不直接用于限制普通变量,而是通过函数返回类型或类型推断来使用。

void

在 TypeScript 中,void 类型表示函数没有返回值,或者说函数返回 undefined。它表示函数执行完毕后没有任何返回值。

严格模式下不能将 null 赋值给 void 类型:

  • TypeScript 的严格模式下,不能将 null 赋值给 void 类型变量。因为 void 类型只能接受 undefined,而不是 null。这符合 void 类型的定义,即函数没有明确的返回值。

void 常用于限制函数返回值:

  • 在函数声明中,当函数没有明确的 return 语句或者返回值时,其返回类型会被推断为 void 类型。这在你希望函数执行完毕后不返回任何值时非常有用。
function logMessage(message: string): void {     console.log(message);     // 没有 return 语句,返回类型被推断为 void } 

总结来说,void 类型用于表示函数没有返回值,不能被赋予除了 undefined 之外的其他值。它是在 TypeScript 中用来强制限制函数的返回值的一种有用工具。

object

1、object 的含义:

object 小写形式在 TypeScript 中表示任何非原始值类型,包括对象、函数、数组等。它是 TypeScript 的一种基础类型之一,限制的范围较宽泛。

let obj: object; obj = {};       // 合法,空对象 obj = []        // 合法,数组 obj = () => {}; // 合法,函数 

由于 object 包含了许多复杂类型,它通常用得比较少,因为具体类型信息更有利于类型检查和代码理解。

2、Object 的含义:

Object 大写形式表示 JavaScript 中的实例对象,即所有非原始类型的对象。这包括普通对象、数组、函数等等。由于它包含了 JavaScript 中几乎所有的对象实例,因此在类型限制时使用 Object 会非常宽泛,通常来说几乎不用。

let obj: Object; obj = {};       // 合法,空对象 obj = [];       // 合法,数组 obj = () => {}; // 合法,函数 

由于 Object 的范围太大,它在类型限制时几乎没有实际应用的意义,因为无法有效地约束具体的对象类型。

3、实际开发中的使用建议:

限制一般对象:通常使用具体的对象字面量类型来限制,例如 {} 表示空对象,或者定义具有特定属性的接口类型。

// 限制person对象的具体内容,使⽤【,】分隔,问号代表可选属性 let person: { name: string, age?: number } // 限制car对象的具体内容,使⽤【;】分隔,必须有price和color属性,其他属性不去限制,有没有都⾏ let car: { price: number; color: string;[k: string]: any } // 限制student对象的具体内容,使⽤【回⻋】分隔 let student: {     id: string     grade: number } // 以下代码均⽆警告 person = { name: '张三', age: 18 } person = { name: '李四' } car = { price: 100, color: '红⾊' } student = { id: 'tetqw76te01', grade: 3 }

4、限制函数的参数、返回值:

使用具体的函数签名来限制函数的参数类型和返回值类型。

function greet(name: string): void {     console.log(`Hello, ${name}!`); } 

5、限制数组:

使用特定的数组类型来限制数组元素的类型。

let numArr: number[]; // 限制为数字类型的数组 let strArr: string[]; // 限制为字符串类型的数组

总结来说,object 用于广泛表示非原始类型的值,而 Object 范围过于宽泛且不常用。

tuple

Tuple(元组)确实是 TypeScript 中的一种特殊数组类型,它具有固定长度和固定类型顺序的特点。在 TypeScript 中,元组允许我们定义一个数组,其中每个位置的元素都有固定的类型。

例如,如果我们想要一个包含字符串和数字的元组,可以这样定义:

let tupleExample: [string, number];  tupleExample = ['hello', 10]; // 合法 tupleExample = [10, 'hello']; // 错误,顺序和类型不匹配 tupleExample = ['hello', 10, 'world']; // 错误,长度不匹配 

在这个例子中,tupleExample 是一个包含两个元素的元组,第一个元素是字符串类型,第二个元素是数字类型。由于元组的长度和类型是固定的,因此在赋值时需要保证元素顺序和类型匹配,并且不能超出指定的长度。

元组在一些特定场景下很有用,例如表示固定长度和特定顺序的数据结构,例如坐标 [x, y] 或者日期 [year, month, day] 等。

enum

Enum(枚举)是 TypeScript 中的一种特殊数据类型,用于定义数值集合,使代码更具可读性和可维护性。枚举类型可以帮助开发者在代码中使用有意义的符号名称来表示数值,而不是直接使用数值或字符串。

// 定义枚举 Direction enum Direction {     Up = 1,     // 从1开始计数     Down,       // 自动递增,Down = 2     Left,       // Left = 3     Right       // Right = 4 }  // 声明一个变量并赋予枚举值 let playerDirection: Direction = Direction.Down;  // 使用 switch 语句处理枚举值 switch (playerDirection) {     case Direction.Up:         console.log("向上移动");         break;     case Direction.Down:         console.log("向下移动");         break;     case Direction.Left:         console.log("向左移动");         break;     case Direction.Right:         console.log("向右移动");         break;     default:         break; } 

⾃定义类型

自定义类型可以让我们更灵活地限制变量的类型,以适应特定需求或约束。在 TypeScript 中,我们可以使用接口(interface)或类型别名(type alias)来自定义类型。

使用接口定义类型

// 定义一个接口来描述一个用户对象的结构 interface User {     id: number;     username: string;     email: string;     age?: number; // 可选属性 }  // 声明一个符合 User 接口的变量 let newUser: User = {     id: 1,     username: "john_doe",     email: "john.doe@example.com",     age: 30 }; 

在上面的例子中,User 接口定义了一个用户对象的结构,包括必须的 id、username 和 email 属性,以及一个可选的 age 属性。通过使用接口,我们可以确保 newUser 变量符合指定的结构,从而提高代码的可读性和可维护性。

使用类型别名定义类型

// 使用类型别名定义一个字符串或数组 type StringOrArray = string | string[];  // 使用类型别名声明一个变量 let data: StringOrArray;  // 此时 data 可以是字符串或字符串数组 data = "hello"; data = ["apple", "banana"]; 

在这个例子中,StringOrArray 是一个类型别名,可以表示字符串或字符串数组。这样的自定义类型别名使得我们可以在多种情况下灵活使用同一种类型约束。

抽象类

抽象类是 TypeScript 中的一种特殊类别,它不能直接实例化,而是作为其他类的基类来使用。抽象类主要用于定义其他类的结构和行为,但本身不会被实例化。

定义抽象类

要定义一个抽象类,需要在类声明前加上 abstract 关键字,并且可以包含抽象方法和非抽象方法。

// 定义一个抽象类 Animal abstract class Animal {     // 抽象方法,没有具体实现,子类必须实现它     abstract makeSound(): void;      // 非抽象方法,具有默认的实现     move(): void {         console.log("roaming the earth...");     } }  // 不能直接实例化抽象类 // let animal = new Animal(); // Error: 无法创建抽象类的实例  // 定义一个继承自抽象类的具体类 Dog class Dog extends Animal {     makeSound(): void {         console.log("Woof! Woof!");     } }  // 创建 Dog 类的实例 let dog = new Dog(); dog.makeSound(); // 输出: Woof! Woof! dog.move();      // 输出: roaming the earth... 

注意

  • 不能被实例化: 抽象类本身不能被实例化,只能作为其他类的父类来使用。
  • 包含抽象方法: 抽象类中可以包含抽象方法,这些方法在子类中必须被实现。
  • 可以包含具体方法: 抽象类中也可以包含具体的方法实现,子类可以选择性地重写这些方法。
  • 用于约束和扩展: 抽象类常用于约束子类的行为和结构,提供了一种模板或蓝图的作用,使得代码更加模块化和可扩展。

接口

接口是用来描述类或对象应该具备的结构,包括属性和方法。类可以实现(implement)接口,从而强制类中包含接口中定义的所有属性和方法。

注意:在 TypeScript 中,接口可以在不同的地方重复声明,但是它们会被合并成单一的接口定义。这种合并的规则包括属性签名的合并以及方法重载的合并。

// 定义一个 Animal 接口 interface Animal {     name: string; // 名称     age: number; // 年龄     speak(): void; // 定义一个抽象的 speak 方法,没有具体实现 }  // 实现接口的类 class Dog implements Animal {     name: string; // 名称     age: number; // 年龄      constructor(name: string, age: number) {         // 将传入的name参数赋值给类的name属性         this.name = name;         // 将传入的age参数赋值给类的age属性         this.age = age;     }      speak(): void {         // 输出带有名字和汪汪的消息         console.log(`${this.name} 说 汪汪!${this.name}今年${this.age}岁了`);     } }  class Cat implements Animal {     name: string; // 名称     age: number; // 年龄      constructor(name: string, age: number) {         // 将传入的name赋值给this.name         this.name = name;         // 将传入的age赋值给this.age         this.age = age;     }      speak(): void {         // 输出字符串,格式为:this.name + " 说 喵喵!"         console.log(`${this.name} 说 喵喵!${this.name}今年${this.age}岁了`);     } }  // 使用实现了接口的类 let dog = new Dog("小白", 3); let cat = new Cat("小花", 2);  dog.speak(); // 输出: 小白 说 汪汪!小白今年3岁了 cat.speak(); // 输出: 小花 说 喵喵!小花今年2岁了

【接口】与【自定义类型】的区别:

接口和自定义类型(也称为类型别名)在功能上有所不同:

接口(Interfaces):

  • 可以描述对象的结构,包括属性、方法、索引签名等。
  • 可以被类实现(implement),从而强制类符合接口定义的结构。
  • 可以被用作类型声明,约束变量、函数或类的结构。

自定义类型(Type Aliases):

  • 仅仅是给一个类型起一个新名字,可以用来简化复杂类型的引用。
  • 不能描述方法或行为,仅用来定义已有类型的别名。

【接口】与【抽象类】的区别:

接口和抽象类都是用来定义规范,但它们有着不同的特性和用途:

抽象类(Abstract Classes):

  • 可以包含普通方法的实现和抽象方法的声明。
  • 可以被继承(extends)。
  • 可以包含构造函数。
  • 可以有访问修饰符(public、protected、private)。

接口(Interfaces):

  • 只能包含抽象方法的声明,没有方法的实现。
  • 类通过实现(implements)接口来强制符合接口定义的结构。
  • 不能包含字段(fields)、构造函数或任何实现代码。

总结来说,接口适合用于描述对象的结构和行为规范,而抽象类则更适合于具备某些默认行为的类的继承和实现。自定义类型则是简化已有类型的引用的一种方式,不具备描述结构或行为的能力。

属性修饰符

在 TypeScript 中,有几种属性修饰符可以用来控制类成员的可访问性和可见性。主要的属性修饰符包括:

  • public:默认修饰符,如果没有指定修饰符,成员被视为 public。public 成员在类内部、子类和类的外部均可访问。
  • private:私有成员只能在定义了这个成员的类的内部访问。即使是继承自该类的子类也不能访问其私有成员。
  • protected:受保护成员在定义了这个成员的类内部及其子类中可访问。但是在类的外部不能访问受保护成员。
  • readonly:只读成员必须在声明时或构造函数内被初始化,并且无法再被修改。可以将类的属性声明为 readonly,这样在实例化后就无法再修改它们的值了。
class Animal {     public name: string;     // 默认为 public     private age: number;     // 私有成员,只能在 Animal 类内部访问     protected habitat: string;  // 受保护成员,可以在 Animal 及其子类内部访问     readonly species: string;   // 只读成员,必须在声明时或构造函数内初始化      /**      * 构造函数      *      * @param name 名称      * @param age 年龄      * @param habitat 栖息地      * @param species 物种      */     constructor(name: string, age: number, habitat: string, species: string) {         this.name = name;         this.age = age;         this.habitat = habitat;         this.species = species;     }      public displayInfo(): void {         console.log(`${this.name} is a ${this.age} years old ${this.species} living in ${this.habitat}.`);     } }  class Dog extends Animal {     constructor(name: string, age: number) {         super(name, age, 'House', 'Dog');         // this.age 是私有成员,子类无法直接访问         // this.habitat 是受保护成员,可以在子类中访问     }      public makeSound(): void {         // 可以访问从父类继承的 public 和 protected 成员         console.log(`${this.name} says Woof!`);         console.log(`${this.name} lives in ${this.habitat}.`);         // console.log(`${this.name} is ${this.age} years old.`); // 错误:age 是私有成员,无法访问     } }  // 测试 let myDog = new Dog('Buddy', 3); myDog.displayInfo(); // 可以访问父类的公共方法 // myDog.age = 4; // 错误:age 是私有成员,无法直接访问 // myDog.species = 'Golden Retriever'; // 错误:species 是只读成员,无法修改 

泛型

定义⼀个函数或类时,有些情况下⽆法确定其中要使⽤的具体类型(返回值、参数、属性的类型不能确
定),此时就需要泛型了
举例: <T> 就是泛型,(不⼀定⾮叫 T ),设置泛型后即可在函数中使⽤ T 来表示该类型:

function test<T>(arg: T): T {     return arg; } // 不指名类型,TS会⾃动推断出来 test(10) // 指名具体的类型 test<number>(10)

在 TypeScript 中,泛型不仅限于单一类型参数,可以定义多个泛型参数来增加函数或类的灵活性。这种能力允许你在一个函数或类中使用多个不同类型的参数,并保持类型安全和通用性。

// 定义一个多个泛型参数的函数 function pair<T, U>(first: T, second: U): [T, U] {     return [first, second]; }  // 使用方式一:明确指定类型 let pair1: [string, number] = pair<string, number>('hello', 5); // 返回 ['hello', 5]  // 使用方式二:类型推断 let pair2 = pair('world', true); // 返回 ['world', true],自动推断为 [string, boolean] 

当然,泛型不仅限于函数,也可以在 TypeScript 类中使用。类中的泛型能够增强类的灵活性,允许你在定义类时不指定具体的数据类型,而是推迟到实例化类的时候再确定类型。

// 定义一个泛型类 class Pair<T, U> {     private first: T;     private second: U;      constructor(first: T, second: U) {         this.first = first;         this.second = second;     }      getFirst(): T {         return this.first;     }      getSecond(): U {         return this.second;     } }  // 使用方式一:明确指定类型 let pair1: Pair<string, number> = new Pair<string, number>('hello', 5); console.log(pair1.getFirst()); // 输出 'hello' console.log(pair1.getSecond()); // 输出 5  // 使用方式二:类型推断 let pair2 = new Pair('world', true); console.log(pair2.getFirst()); // 输出 'world',自动推断为 string console.log(pair2.getSecond()); // 输出 true,自动推断为 boolean 

当然也可以对泛型的范围进⾏约束。通过约束泛型,你可以限制泛型参数必须是某种特定类型,从而在编译时捕获一些错误,同时仍然保持灵活性。

// 定义一个接口,用于约束泛型类型必须具备的属性 interface Printable {     print(): void; }  // 泛型类,约束泛型参数必须实现 Printable 接口 class Printer<T extends Printable> {     private item: T;      constructor(item: T) {         this.item = item;     }      printItem() {         this.item.print();     } }  // 实现 Printable 接口的具体类 class Book implements Printable {     constructor(private title: string) {}      print() {         console.log(`打印书籍:${this.title}`);     } }  class Car {     constructor(private brand: string) {}      // 这个类没有实现 Printable 接口 }  // 使用泛型类 Printer,并传入符合约束的类型 let bookPrinter = new Printer<Book>(new Book('TypeScript手册')); bookPrinter.printItem(); // 输出 "打印书籍:TypeScript手册"  // 如果尝试传入不符合约束的类型将会报错 // let carPrinter = new Printer<Car>(new Car('Toyota')); // 这行代码会导致编译时错误