类型兼容性 - TypeScript 标注类型
类型兼容性
TypeScript 中的类型兼容性是基于结构类型系统的。结构类型系统是一种只使用其成员来表达类型的方式。它正好与名义(nominal)类型系统形成对比。
interface Pet { name: string; } class Dog { name: string; } let pet: Pet; // OK, because of structural typing pet = new Dog();
在使用基于名义类型系统的语言,比如 C#或 Java 中,这段代码会报错,因为Dog
类没有明确说明其实现了Pet
接口。 TypeScript 的结构型类型系统是根据 JavaScript 代码的典型写法来设计的。因为 JavaScript 里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更自然。
关于可靠性的注意事项
TypeScript 的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript 允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。
开始
TypeScript 结构类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的成员。例如,思考以下代码,该代码涉及一个名为Pet
的接口,接口拥有一个名为name
的属性:
interface Pet { name: string; } let pet: Pet; let dog = { name: "Lassie", owner: "Rudd Weatherwax" }; pet = dog; // dog's inferred type is { name: string; owner: string; }
为了检查dog
类型是否可以赋值给pet
类型,编译器将检查pet
的每个属性,并在dog
中查看是否有对应的兼容属性。在本例中,dog
必须拥有一个叫做name
的string
类型成员属性。dog
拥有该属性,所以检查通过,赋值被允许发生。该赋值规则同样用于检查函数调用参数:
interface Pet { name: string; } let dog = { name: "Lassie", owner: "Rudd Weatherwax" }; function greet(pet: Pet) { console.log("Hello, " + pet.name); } greet(dog); // OK
注意,dog
有个额外的owner
属性,但这不会引发错误。只有目标类型(本例中为Pet)的成员会在兼容性检查中被考虑这个比较过程是递归进行的,将检查每个成员及子成员的类型。
比较两个函数
相对来讲,在比较基本类型和对象类型的时候是比较容易理解的,但如何判断两个函数是兼容的却略微复杂。下面我们从两个简单的函数入手,它们仅是参数列表略有不同:
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error
要查看x
是否能赋值给y
,首先看它们的参数列表。x
的每个参数必须能在y
里找到对应兼容类型的参数。注意,参数的名字相同与否并无所谓,我们只关注它们的类型。在第一个赋值中,x
的每个参数在y
中都能找到对应的参数,所以允许赋值。而第二个赋值错误,则是因为y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
你可能会疑惑为什么允许忽略参数,像例子y = x
中那样。原因是忽略额外的参数在 JavaScript 里是很常见的。例如,Array#forEach
给回调函数传 3 个参数:数组元素,索引和整个数组。尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:
let items = [1, 2, 3]; // 并不强制额外的参数 items.forEach((item, index, array) => console.log(item)); // 这样也是可以的! items.forEach((item) => console.log(item));
下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:
let x = () => ({ name: "Alice" }); let y = () => ({ name: "Alice", location: "Seattle" }); x = y; // OK y = x; // Error, 因为 x() 缺少 location 属性
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
函数参数差别
当比较函数参数类型时,如果源参数可分配给目标参数,则分配成功,反之亦然。这是不合理的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。在实践中,这种错误是罕见的,允许这种模式是为了兼容 JavaScript 中许多常见模式。例如:
enum EventType { Mouse, Keyboard, } interface Event { timestamp: number; } interface MyMouseEvent extends Event { x: number; y: number; } interface MyKeyEvent extends Event { keyCode: number; } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ } // Unsound, but useful and common listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y)); // Undesirable alternatives in presence of soundness listenEvent(EventType.Mouse, (e: Event) => console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y) ); listenEvent(EventType.Mouse, ((e: MyMouseEvent) => console.log(e.x + "," + e.y)) as (e: Event) => void); // Still disallowed (clear error). Type safety enforced for wholly incompatible types listenEvent(EventType.Mouse, (e: number) => console.log(e));
你可以使用strictFunctionTypes
编译选项,使 TypeScript 在这种情况下报错。注意,当strict
选项为true
时,默认开启该选项。
可选参数与剩余参数
比较函数兼容性的时候,可选参数与剩余参数是可互换的。源类型上有额外的可选参数不会产生错误,目标类型的可选参数在源类型里没有对应的参数也不会产生错误。当一个函数有剩余参数时,它被当做无限个可选参数。这对于类型系统的判断来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
。
有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:
function invokeLater(args: any[], callback: (...args: any[]) => void) { /* ... 使用参数执行回调函数 ... */ } // 不可靠 - invokeLater "可能" 提供任意数量的参数 invokeLater([1, 2], (x, y) => console.log(x + ", " + y)); // 干扰:x 和 y 是切实需要的,但由于是可选参数,无法发现该错误 invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。这确保了目标函数可以在所有源函数可调用的地方调用。
枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。例如:
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
类的私有成员和受保护成员
类的私有成员和受保护成员会影响兼容性。当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。同样地,这条规则也适用于包含受保护成员实例的类型检查。这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。例如:
interface Empty {} let x: Empty; let y: Empty; x = y; // OK, 因为y和x的结构相匹配
上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty { data: T; } let x: NotEmpty; let y: NotEmpty; x = y; // Error, 因为 x 和 y 不兼容
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。然后用结果类型进行比较,就像上面第一个例子。例如:
let identity = function (x: T): T { // ... }; let reverse = function (y: U): U { // ... }; identity = reverse; // OK, 因为 (x: any) => any 匹配 (y: any) => any
目前为止,我们使用了“兼容性”,它在语言规范里没有定义。在TypeScript里,有两种兼容性:子类型和赋值。它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。语言里的不同地方分别使用了它们之中的机制。实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。
Any、unknown、object、void、undefined、null 和 never 的赋值性
赋值性表格
重申基础
- 所有类型的值可赋值给其自身类型的变量。
- any 和 unknown 的值在赋值给其他类型的变量时表现相同;不同在于当它作为变量类型时,不可被赋值any以外的任何类型的变量。
- unknown 和 never 的表现接近于互相相反。所有类型的变量可被赋值unknown类型的值, never类型的变量可被赋值任意类型的值。任意类型的变量不可被赋值never类型的值, unknown类型的变量不可以被(any类型以外的)任意类型的值赋值。
- void类型总是不可赋值或被赋值,除以下的例外情况: 1、当void类型作为变量时,仅可被赋值any、unknown类型的值;2、当void类型作为值时,仅可赋值给never、undefined和null类型的变量(当strictNullChecks被关闭,点击链接查看详情).
- 当 strictNullChecks被关闭, null 和 undefined 的表现与 never 相似:作为变量可被赋值大部分类型的值,作为值不可赋值给大部分类型的变量,他们可以相互赋值。
- 当 strictNullChecks被开启, null 和 undefined 的表现类似于 void:总是不可赋值或被赋值,除以下的例外情况:1、作为变量类型时,仅可被赋值any和unknown类型的值;2、作为值时,仅可赋值给Never类型的值;3、undefined类型的变量总是可被赋值void 类型的值。
相关阅读:strictNullChecks
默认:当strict选项开启时默认开启,其他时候默认关闭相关:strict当strictNullChecks为假时,null和undefined实际上会被语言所忽视,而这可能导致未期的错误。当strictNullChecks为真时,null和undefined将拥有自身的显著而确切的类型,此时若你在需要实际值的地方使用他们,将会产生类型错误。
例如下列代码,users.find无法保证它一定能寻找到用户,但你可以在假设它可以找到的情况下编写代码:
declare const loggedInUsername: string; const users = [ { name: "Oby", age: 12 }, { name: "Heera", age: 32 }, ]; const loggedInUser = users.find((u) => u.name === loggedInUsername); console.log(loggedInUser.age);
设置strictNullChecks为true时,将在你无法保证loggedInUser存在的前提下,产生一个错误以阻止你的尝试使用。
declare const loggedInUsername: string; const users = [ { name: "Oby", age: 12 }, { name: "Heera", age: 32 }, ]; const loggedInUser = users.find((u) => u.name === loggedInUsername); console.log(loggedInUser.age); //错误:对象loggedInUser可能为'undefined'.
第二个例子的错误原因——源于Array的find方法——可以如下简化说明:
// 当 strictNullChecks 为 true type Array = { find(predicate: (value: any, index: number) => boolean): S | undefined; }; // 当 strictNullChecks 为 false,undefined 被从类型系统中移除以允许你在假设其 // 总是返回一个结果的情况下编写代码 type Array = { find(predicate: (value: any, index: number) => boolean): S; };
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!