跳至主要內容

类型兼容性

樱桃茶大约 25 分钟

类型兼容性

类型兼容性在 TypeScript 中是一项基础概念,用于描述当一个类型的变量能否被分配给另一个类型的变量时应遵循的规则。这里有几个关键点需要理解:

  • TypeScript 的类型兼容性是基于结构子类型的。如果类型 Y 有类型 X 所有的属性和方法,那么 X 可以被认为是 Y 的子类型。
  • 当一个类型系统具有这样的特性时,它会检查值所具有的形状,而不是它们的显式类型。

以下是类型兼容性的一些实用例子:

  1. 接口兼容性

    • 假设你有两个接口,Interface A 有属性 xyInterface B 只有属性 x

      interface A {
        x: number;
        y: number;
      }
      
      interface B {
        x: number;
      }
      
      let a: A = { x: 1, y: 2 };
      let b: B = { x: 3 };
      
      // B 可以赋值给 A,因为B至少有A所有的属性。
      a = b; // 没问题
      
  2. 函数兼容性

    • 函数兼容性主要考虑参数列表和返回值类型:

      let fn1 = (a: number) => 0;
      let fn2 = (b: number, s: string) => 0;
      
      fn2 = fn1; // 没问题,TypeScript使用鸭式辩型法,fn1的每个参数都能在fn2中找到对应。
      // fn1 = fn2; // 错误,fn2的每个参数找不到在fn1中的对应项。
      
  3. 枚举兼容性

    • 枚举与数字类型互相兼容,但各个枚举之间不兼容:

      enum Status {
        Uploading,
        Success,
        Failed,
      }
      let status = Status.Uploading;
      
      status = 1; // 数字赋值给枚举,没问题。
      // status = "string"; // 错误,字符串不能赋值给枚举。
      
  4. 类兼容性

    • 类与对象字面量和接口兼容性很像,主要考虑其实例成员和方法:

      class Animal {
        feet: number;
        constructor(name: string, numFeet: number) {}
      }
      
      class Size {
        feet: number;
        constructor(meters: number) {}
      }
      
      let a: Animal;
      let s: Size;
      
      a = s; // 没问题,因为s有a所有的属性。
      // s = a; // 同样没问题。
      

记住,TypeScript 的类型兼容性是基于结构型类型系统,这意味着当比较两个不同的类型时,TypeScript 会检查这两个类型的结构,而非它们声明时的类型名称。如果两个类型的内部结构"看起来"是兼容的,那么它们就是兼容的。

关于可靠性的注意事项

类型兼容性是 TypeScript 中一个非常核心的概念,它涉及到 TypeScript 如何在类型之间进行检查以确保代码的安全性。在 TypeScript 官方文档中提到“关于可靠性的注意事项”,主要是在讲述 TypeScript 类型系统如何处理类型赋值时的一些细节和考虑,以确保类型的兼容性。以下是这个部分的解释:

  • TypeScript 的类型兼容性是基于结构子类型的。如果一个类型的所有成员都能在另一个类型中找到对应的成员,那么这两个类型就是兼容的。

  • 在判断兼容性时,TypeScript 会考虑变量的每个属性和方法。如果一个对象可以被认为是另一个对象的子集,那么这两个对象被认为是类型兼容的。

  • 额外的属性检查:当你将对象字面量赋值给变量或者作为参数传递时,TypeScript 会检查是否存在多余的属性。例如:

    interface Expected {
      x: number;
    }
    
    let myVar: Expected = { x: 10, y: 20 }; // 错误,因为 'y' 属性在 'Expected' 类型中不存在
    
  • 函数参数双向协变:函数的参数类型不仅需要匹配目标类型参数的类型,而且反过来也需要匹配。这意味着函数参数的类型允许更具体或者更抽象,这在某些情况下可能导致运行时错误。

  • 返回类型的协变:函数的返回类型则是单向的,只需要源函数的返回类型可以赋值给目标函数的返回类型即可。

  • 可选参数与剩余参数:函数的可选参数和剩余参数在比较时有特别的兼容性规则。如果一个函数有额外的可选参数,在类型检查时通常不会报错。

  • 枚举之间的兼容性:TypeScript 中的枚举类型和数字类型是兼容的,但不同枚举类型之间是不兼容的。

理解这些原则和规则对学习 TypeScript 至关重要,因为它们直接影响了你的代码如何通过类型系统的校验。实践中,你应该时刻关注自己定义的类型是否符合预期的结构,并且尝试编写既健壮又安全的类型定义。通过不断的练习和阅读相关的错误信息,你会渐渐熟悉 TypeScript 的类型兼容性原则。

开始

类型兼容性是 TypeScript 中的一个核心概念,它让类型系统的灵活性和安全性得到平衡。这里的“类型兼容性”指的是在赋值或者比较类型时,TypeScript 编译器如何确定一个类型是否可以视为另一个类型的子类型。

在 TypeScript 中,如果我们有两个不同的类型 A 和 B,类型 A 可以被认为是类型 B 的子类型,如果 A 至少包含 B 所有的必要属性。具体来说,这涉及以下几个方面:

  • 结构兼容性:TypeScript 是基于结构类型系统的,这意味着类型的兼容性是由其成员决定的,而不是它们声明时的名字或位置。这与 JavaScript 的弱类型系统相对应,使得 JavaScript 对象可以轻松的适配新的模式。

    示例:

    interface Named {
      name: string;
    }
    
    class Person {
      name: string;
    }
    
    let p: Named;
    // OK, because of structural typing
    p = new Person();
    

    在上述例子中,Person 类型的对象可以分配给 Named 接口的变量,因为 Person 具有所有 Named 需要的属性。

  • 函数兼容性:当比较两个函数的类型兼容性时,主要看参数列表和返回值。函数的参数列表使用 "逆变"(contravariance),即比较函数参数类型时,每个参数都应该能够接受父类型所能接受的所有类型。而返回值使用 "协变"(covariance),即函数的返回值类型必须至少是声明类型的返回值类型或其子类型。

    示例:

    let x = (a: number) => 0;
    let y = (b: number, s: string) => 0;
    
    y = x; // OK
    // x = y; // Error
    

    这里 x 能被赋值给 y 因为 x 的参数是 y 参数的子集,并且返回值类型(void)是相匹配的。

  • 枚举兼容性:枚举和数字类型是相互兼容的。不同枚举之间不兼容。

    示例:

    enum Status {
      Ready,
      Waiting,
    }
    enum Color {
      Red,
      Blue,
      Green,
    }
    
    let status = Status.Ready;
    status = Color.Green; // Error: 不同枚举之间不兼容
    
  • 类兼容性:类的兼容性和接口/字面量的兼容性非常相似,但需要考虑静态部分和实例部分。实例部分是基于结构的,而静态部分则不参与比较。

    示例:

    class Animal {
      feet: number;
      constructor(name: string, numFeet: number) {}
    }
    
    class Size {
      feet: number;
      constructor(numFeet: number) {}
    }
    
    let a: Animal;
    let s: Size;
    
    a = s; // OK
    s = a; // OK
    

    上面的示例中,尽管 AnimalSize 类的构造函数不同,但它们的实例部分是相似的,所以它们是兼容的。

理解 TypeScript 的类型兼容性对于编写大型应用程序和维护项目的类型安全至关重要。通过了解和利用类型兼容性原则,开发者可以确保他们的代码能够有效地处理各种类型,同时仍然享有类型检查带来的好处。

比较两个函数

类型兼容性是 TypeScript 中用于确定一个类型是否可以被赋值给另一个类型的规则体系。在比较两个函数时,主要考虑函数参数(也称为"函数签名")和返回类型。

  • 参数列表比较(Parameter List Comparison):

    • 在 TypeScript 中,参数列表使用基于名称的比较方式。
    • 若两个函数的每个参数类型相同,且参数数量也相同,则这两个函数兼容。
    • 参数可以被视为左向右进行匹配,每个参数类型必须相对应。
    • 函数的参数数量可能不同。如果一个函数具有更少的参数,则它可以兼容拥有更多参数的函数,因为额外的参数会被忽略。这种情况被称为"函数参数的可选性"或"参数省略"。
  • 返回类型比较(Return Type Comparison):

    • 如果两个函数的返回类型相同,那么他们在返回类型方面是兼容的。
    • 返回类型应当是协变的,这意味着派生类型的函数可以替换掉基类型的函数。

以下是几个例子,说明不同情况下函数的类型兼容性:

// 假设我们有两个函数类型:x 和 y

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK, x 的每个参数都能在 y 中找到对应的参数
// x = y; 这将出错,因为 y 有一个 x 中不存在的额外参数

let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });

x = y; // OK,y 的返回类型是 x 的返回类型的子类型
// y = x; 这将出错,因为 x 的返回类型不能赋值给 y 的返回类型
  • 在比较带有参数的函数时,如果源函数的参数能在目标函数中找到对应参数,并且后者的每个参数类型都至少与前者的参数类型一样宽泛,那么就认为这两个函数是兼容的。
  • 当比较返回类型时,需要确保源函数的返回类型至少与目标函数的返回类型一样严格,或者说目标函数的返回类型是源函数返回类型的子集。

通过理解 TypeScript 中的这些函数兼容性原则,可以帮助你更好地理解如何在 TypeScript 中定义和使用函数,特别是当处理高阶函数或回调时。

函数参数双向协变

函数参数双向协变指的是在 TypeScript 中,函数参数类型的比较有一定的灵活性。具体来说,当比较两个函数的兼容性时,TypeScript 会以一种“双向协变”的方式去检查函数的参数类型:

  • 协变(Covariance):通常指子类型关系可以从基类型传递到复合类型(例如,如果Type AType B的子类型,那么Type A[]也应该是Type B[]的子类型)。
  • 逆变(Contravariance):与协变相反,指父类型关系可以从子类型传递到复合类型(例如,如果Type AType B的子类型,那么Consumer<Type B>应该是Consumer<Type A>的子类型)。

在 TypeScript 里,函数参数的类型检查既不是完全协变也不是完全逆变,而是被设计为“双向协变”,这意味着在某些情况下 TypeScript 会同时采用协变和逆变规则,这样做虽然增加了灵活性,但也降低了类型安全性。以下是一些实用的例子:

// 假设我们有两个接口和一个函数:
interface Animal {
  name: string;
}

interface Dog extends Animal {
  bark(): void;
}

function train(animal: Animal): void {
  // ...
}

// 另外一个函数,它的参数是一个更具体的类型(Dog):
function trainDog(dog: Dog): void {
  // ...
}

// TypeScript允许你将trainDog赋值给一个需要Animal参数的函数类型
let trainer: (animal: Animal) => void = trainDog;
// 这里发生了双向协变的参数类型检查,Dog是Animal的子类型,所以从类型安全的角度看似乎没有问题

// 然而,双向协变在执行期间可能导致问题:
trainer({ name: "Generic Animal" }); // 运行时错误,因为该对象没有bark方法,但在编译阶段不会报错

在上面的例子中,即使trainDog函数需要一个Dog类型的参数,TypeScript 仍然允许我们将其赋值给期望Animal类型参数的trainer变量。这在编译阶段不会报错,但如果在执行时传入一个普通的Animal对象到trainer函数,就会在运行时引发错误,因为只有Dog才有bark方法。

尽管 TypeScript 的这种设计提供了便利,但作为开发者,你应该意识到其中潜在的类型安全问题,并在实际编码过程中谨慎处理这类赋值。

可选参数及剩余参数

在 TypeScript 中理解函数间的类型兼容性,特别是涉及到可选参数和剩余参数时,是非常重要的。

  • 可选参数和剩余参数如何影响类型兼容性:

    • TypeScript 中的函数可以有可选参数(使用?标记)和剩余参数(使用...操作符)。这些特性影响着函数之间的兼容性。
  • 可选参数:

    • 当比较两个含有可选参数的函数时,带有更少可选参数的函数被认为是不兼容的。这是因为缺少参数的函数不能保证能处理更多的参数。
    • 例子:
      let funcA = (x: number, y?: number) => {};
      let funcB = (x: number) => {};
      // funcB 可以赋值给 funcA,因为 funcA 期望的参数 funcB 都能满足
      funcA = funcB; // 兼容
      // funcA 不能赋值给 funcB,因为 funcB 不接受第二个参数
      // funcB = funcA; // TypeScript 会报错,不兼容
      
  • 剩余参数:

    • 函数可以通过剩余参数来接受任意数量的参数。当一个函数有剩余参数时,它可以被认为是与另一个没有剩余参数但参数类型相同的函数兼容。

    • 例子:

      function buildName(firstName: string, ...restOfName: string[]) {
        return firstName + " " + restOfName.join(" ");
      }
      
      let buildNameFn: (fname: string, ...rest: string[]) => string = buildName;
      // 这里表明了具有剩余参数的 buildName 函数可以被赋值给一个期待同样参数的函数类型变量。
      
  • 总结:

    • 可选参数和剩余参数在 TypeScript 中增加了函数定义的灵活性,同时也影响了函数间的类型兼容性。
    • 一个关键的规则是:函数参数的兼容性是基于其参数能否兼容另一个函数的参数。即,如果函数 A 的每个参数都能在函数 B 中找到对应的兼容类型的参数,则函数 A 和函数 B 是兼容的。
    • 在实践中,这意味着你需要注意函数参数的顺序、数量以及它们是否可选,来确保类型兼容性。

函数重载

在 TypeScript 中,函数重载是指为同一个函数提供多个函数类型定义。这样做的目的是使得函数能够处理不同类型的参数,或者当函数根据传入参数的不同而返回不同类型时,能够更准确地表达这种多态性。

当比较两个具有重载的函数时,TypeScript 会按照一定的规则来检查这两个函数的兼容性:

  • TypeScript 会先比较最后一个重载签名。因为在实现中只会有一个函数体,所以这个签名必须兼容所有重载签名。
  • 在比较函数重载时,每个重载签名都必须被至少一个其他函数的重载签名所匹配。

让我们通过一些例子来解释这些概念:

// 为同一个函数提供两个重载签名
function greet(name: string): string;
function greet(age: number): string;

// 函数实现 - 实现签名通常是不可见的,并且不能用于直接匹配
function greet(nameOrAge: any): string {
  if (typeof nameOrAge === "string") {
    return `Hello, ${nameOrAge}`;
  } else if (typeof nameOrAge === "number") {
    return `Your age is ${nameOrAge}`;
  }
}

// 使用函数重载
const greeting1 = greet("Alice"); // 调用第一个重载签名
const greeting2 = greet(30); // 调用第二个重载签名

// 函数签名兼容性示例
let greetFn: (x: string) => string = greet; // 兼容

// 错误的使用方式 - 不匹配任何一个重载签名
let greetFnWrong: (x: boolean) => string = greet; // 错误:类型不兼容

在上面的例子中,greetFn 赋值操作是有效的,因为 (x: string) => string 匹配了 greet 的第一个重载签名。但是 greetFnWrong 赋值操作是无效的,因为没有一个重载签名匹配 (x: boolean) => string 类型。

在设计函数重载时,应该从最特定(即参数最具体)的签名开始声明,然后逐渐到最不特定的签名。这样做是因为 TypeScript 在解析调用的重载时会从上到下依次匹配重载列表,找到第一个匹配的签名就停止搜索。如果将最宽泛的签名放在前面,它可能会捕获所有的调用,导致更具体的重载签名永远不会被匹配。

枚举

类型兼容性是 TypeScript 中一个核心概念,它指的是在赋值、函数调用等场景下,源类型是否可以被看作目标类型。TypeScript 的类型兼容性基于结构子类型的概念,即主要看对象的形状和能力。

在 TypeScript 中,枚举与类型兼容性相关的规则如下:

  • 枚举之间是不兼容的:即使两个枚举内的成员完全一样,他们也不被认为是相同的类型。

    enum Status {
      Ready,
      Waiting,
    }
    enum Color {
      Red,
      Blue,
      Green,
    }
    
    let status = Status.Ready;
    // status = Color.Red; // Error: Type 'Color.Red' is not assignable to type 'Status'.
    
  • 数字枚举与数字类型互相兼容:你可以将任何数字赋值给枚举类型,同时你也可以将数字枚举的成员赋值给数字类型的变量。

    enum Status {
      Ready,
      Waiting,
    }
    let num: number = Status.Ready; // OK because enum member is a number
    let status: Status = 2; // OK because the value '2' is a number
    
  • 字符串枚举与字符串类型不兼容:即便字符串枚举的值实际上是字符串,它们仍然并不认为是普通的字符串类型。

    enum StringEnum {
      Hello = "Hello",
      World = "World",
    }
    let hello: string = "Hello";
    // let myVar: StringEnum = hello; // Error: Type 'string' is not assignable to type 'StringEnum'.
    

理解这些规则有助于避免类型错误,特别是当你从 JavaScript 迁移到 TypeScript 并习惯于 JS 更宽松的类型系统时。这些规则确保了代码更加安全和可靠,在编译期间就能捕获潜在的问题。

在 TypeScript 中,"类"的类型兼容性是基于结构子类型的。这意味着如果一个对象至少拥有另一个对象所需的所有属性和方法,则它们被认为是兼容的。下面用一些例子来说明这一点:

  • 类的实例之间的比较主要看其结构,而非具体的类名。

    class Animal {
      feet: number;
      constructor(name: string, numFeet: number) {}
    }
    
    class Size {
      feet: number;
      constructor(meters: number) {}
    }
    
    let a: Animal = new Animal("dog", 4);
    let s: Size = new Size(2);
    
    // 这里不会报错,因为 s 和 a 的结构是相似的,都有一个名为 feet 的属性
    a = s;
    
  • 私有成员和受保护成员会影响类型兼容性。当一个类含有私有或受保护的成员时,只有来自同一类的其他类才被认为是兼容的。

    class AnimalWithPrivate {
      private name: string;
      constructor(theName: string) {
        this.name = theName;
      }
    }
    
    class Rhino extends AnimalWithPrivate {
      constructor() {
        super("Rhino");
      }
    }
    
    class Employee {
      private name: string;
      constructor(theName: string) {
        this.name = theName;
      }
    }
    
    let animal = new AnimalWithPrivate("Goat");
    let rhino = new Rhino();
    let employee = new Employee("Bob");
    
    animal = rhino; // 正确:Rhino 派生自包含私有成员 name 的 AnimalWithPrivate
    // animal = employee; // 错误: Employee 不是 AnimalWithPrivate 的子类
    
  • 如果两个对象之间有相同的私有或受保护的成员来源,则它们也是兼容的。

    class Person {
      protected name: string;
      constructor(name: string) {
        this.name = name;
      }
    }
    
    class Employee extends Person {
      private department: string;
      constructor(name: string, department: string) {
        super(name);
        this.department = department;
      }
    }
    
    let p = new Person("Alice");
    let e = new Employee("Alice", "Sales");
    
    // 这里不会报错,因为 Employee 继承自 Person,具有相同的受保护成员 name
    p = e;
    

以上就是 TypeScript 官网文档中“类型兼容性 > 类”部分的简化解释。通过这些例子,你可以看到 TypeScript 是如何处理不同类及其实例之间的兼容性问题的。

类的私有成员和受保护成员

TypeScript 中关于类的私有成员和受保护成员的类型兼容性,主要说明了如何处理类中这些特殊访问权限的属性或方法。以下是一些关键点:

  • 私有成员(Private Members)

    • 在 TypeScript 中,类的私有成员只能在其定义的类内部访问。

    • 如果两个类具有相同的私有成员名称,它们不被认为是兼容的。即使他们结构相同,也会因为私有成员的访问限制而认定为不兼容。

      class Animal {
        private name: string;
      
        constructor(theName: string) {
          this.name = theName;
        }
      }
      
      class Rhino extends Animal {
        constructor() {
          super("Rhino");
        }
      }
      
      class Employee {
        private name: string;
      
        constructor(theName: string) {
          this.name = theName;
        }
      }
      
      let animal = new Animal("Goat");
      let rhino = new Rhino();
      let employee = new Employee("Bob");
      
      animal = rhino; // 允许:Rhino 类型对象赋值给 Animal 类型对象
      animal = employee; // 错误:Employee 类型对象不能赋值给 Animal 类型对象,尽管它们都有一个私有的 'name' 成员
      
  • 受保护成员(Protected Members)

    • 受保护成员在类本身及派生类中可以被访问,与私有成员不同,它允许更广泛的类内使用。

    • 类似于私有成员,如果两个类中包含相同的受保护成员,则这两个类不被认为是兼容的,除非它们至少有一个共同的父类。

      class Person {
        protected name: string;
      
        constructor(name: string) {
          this.name = name;
        }
      }
      
      class Employee extends Person {
        private department: string;
      
        constructor(name: string, department: string) {
          super(name);
          this.department = department;
        }
      }
      
      let person = new Person("Jane");
      let employee = new Employee("Bob", "Sales");
      
      person = employee; // 允许:Employee 类型对象赋值给 Person 类型对象
      // 因为 Employee 是由 Person 派生出来的
      

总的来说,当你在 TypeScript 中使用类时,私有成员和受保护成员决定了你的类如何与其他类进行交互。私有成员严格限制了类的封装和独立性,而受保护成员提供了一定程度的封装同时允许继承。理解这些概念对于编写类型安全的代码非常重要。

泛型

类型兼容性是 TypeScript 中的一个核心概念,它描述了当一个类型可以被认为是另一个类型的子类型时的规则。这在处理泛型时尤其重要。

  • 泛型(Generics)是 TypeScript 强大的特性之一,允许你创建可适用于多种数据类型的组件。想象一个盒子,可以装任何东西,但一旦装入某样东西,就只能装那个特定的东西。
  • 在理解类型兼容性的上下文中,泛型让我们能够保持类型信息,这样我们就可以在编译时期检查到类型错误。

举一个简单的例子来说明泛型和类型兼容性:

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

let output1 = identity<string>("myString"); // type of output will be 'string'
let output2 = identity<number>(42); // type of output will be 'number'

在这个例子中,identity 函数是一个泛型函数,因为它可以工作于任何类型 T。调用 identity 时,可以指定 T 是任何想要的类型,从而决定函数返回值的类型。

  • 当 TS 检查泛型代码的类型兼容性时,它会考虑这个泛型类型在具体使用时是否兼容。
  • 如果一个泛型类型没有在其操作中实际使用其类型参数,则不同的泛型类型之间可能会认为是兼容的。

例如:

interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;

x = y; // OK, because y matches the structure of x

即使 xy 是不同的泛型类型(一个是 Empty<number>,另一个是 Empty<string>),TypeScript 认为它们是兼容的,因为接口 Empty<T> 不使用类型参数 T 做任何事情。

  • 另一方面,如果我们开始使用类型参数进行操作,情况就会改变。TypeScript 将认为具有不同类型参数的两个泛型类型是不兼容的。
interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

// x = y; // Error, because x and y are not compatible (Type 'string' is not assignable to type 'number'.)

在此示例中,由于 NotEmpty<T> 实际上使用了类型参数 T(在属性 data 中),所以具有不同类型参数的 NotEmpty<number>NotEmpty<string> 被认为是不兼容的。

通过这些例子,可以看出泛型的类型兼容性取决于泛型类型如何使用其类型参数。如果它们在结构上相似并且不涉及类型参数的具体使用,它们可能是互相兼容的;如果它们的结构因类型参数而不同,则不兼容。

高级主题

类型兼容性是 TypeScript 设计的核心概念之一,它使得 TypeScript 在保持强类型的同时,也能提供灵活的编程方式。在 TypeScript 中,如果一个类型可以被认为是与另一个类型相同的,那么这两个类型就是兼容的。这里主要从“高级主题”中的“类型兼容性”部分来讲解这个概念。

  • 结构性类型系统:TypeScript 使用结构性类型系统。这意味着类型的兼容性并不是基于类型本身是否相同或者类型的名称,而是基于类型的成员。这与名义性类型系统(如 Java 或 C# 中的)形成对比,后者依赖于类型的显式声明和名称。

    • 例子:如果有两个不同的接口,InterfaceAInterfaceB,只要它们的结构(即所含属性)相同,则它们在 TypeScript 中被认为是兼容的。
  • 函数兼容性:涉及到函数时,TypeScript 会检查参数列表和返回值以确定兼容性。

    • 参数兼容性:TypeScript 使用“鸭子类型”或“面向接口编程”,意味着只要函数的每个参数都能找到对应的参数,并且类型相兼容,那么这个函数就是兼容的。不过,要注意的是,函数的参数列表还遵循“参数少的兼容参数多的”原则。
      • 例:(a: number) => void 兼容 (a: number, b: string) => void,因为前者可以适配后者需要的参数类型。
    • 返回值兼容性:函数的返回值类型必须相同或者子类型兼容父类型。
      • 例:如果函数 A 的返回值类型是 number,而函数 B 的返回值类型是 any,那么函数 A 和函数 B 是兼容的,因为 numberany 的子类型。
  • 泛型兼容性:当处理泛型时,TypeScript 只在实例化时考虑类型参数,空的泛型类型参数被视为任何类型(any)。

    • 如果一个泛型类型没有指定类型参数,或者两个泛型类型的定义相同并且他们的类型参数都相同,则它们是兼容的。
      • 例:Array<number>Array<number> 是兼容的;Array<number>Array<string> 不是兼容的。

通过理解 TypeScript 类型系统的这些核心原则,你可以更加自如地利用 TypeScript 提供的类型检查功能,进而写出更安全、更易维护的代码。记住,TypeScript 的目标之一就是尽量减少运行时错误,通过在编译阶段捕捉可能的问题来实现这一点。

子类型与赋值

在 TypeScript 中,子类型与赋值的概念主要涉及到如何判断一个类型是否可以被认为是另一个类型的子类型,以及一个类型的值在什么情况下可以被赋给另一个类型的变量。以下是几个关键点来解释这个概念:

  • 结构性类型系统:TypeScript 使用的是结构性类型系统,意味着如果一个对象的形状兼容,即使不是同一个类或接口创建的,它们也被认为是相同的类型。

  • 子类型:在类型论中,如果类型 A 的每个实例也是类型 B 的实例,那么类型 A 被认为是类型 B 的子类型。在 TypeScript 中,这意味着 A 类型的对象可以在需要 B 类型的地方使用。

  • 赋值:赋值规则决定了当你尝试将一个值赋给变量时,其类型是否兼容。在 TypeScript 中,如果类型 A 可以被赋值给类型 B ,那么类型 A 应该是类型 B 的子类型或相同的类型。

举几个例子说明:

interface Animal {
  name: string;
}

interface Dog extends Animal {
  bark(): void;
}

let animal: Animal = { name: "elephant" };
let dog: Dog = { name: "fido", bark: () => console.log("woof") };

// 由于 Dog 是 Animal 的子类型,因此可以将 dog 赋值给 animal
animal = dog; // 正常工作

// 但是相反不行,因为 Animal 不是 Dog 的子类型,缺少 `bark` 方法
dog = animal; // 错误:Type 'Animal' is not assignable to type 'Dog'.

在上例中,Dog 类型拥有 Animal 类型的所有属性,并且还有自己的额外方法 bark。所以 Dog 可以被认为是 Animal 的子类型,你可以把一个 Dog 实例赋值给一个 Animal 类型的变量。但是反过来则不行,因为 Animal 类型的对象不一定有 bark 方法。

  • 可选属性和赋值:当一个类型具有可选属性时,赋值兼容性会成为一个细微的问题。
interface Person {
  name: string;
  age?: number; // 可选属性
}

let employee: Person = { name: "Alice" };
let customer: Person = { name: "Bob", age: 23 };

// 这种赋值是合法的,因为忽略可选属性是允许的
employee = customer;

// 这也是合法的,尽管 `age` 属性不存在
customer = employee;

在上例中,Person 接口定义了一个可选属性 age,这意味着即使 age 属性不存在,也可以认为一个对象符合 Person 类型。

理解 TypeScript 中的子类型和赋值规则对于掌握类型检查非常重要,这直接关系到你的代码在编译时能否通过检查,以及运行时的安全性。