TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
这里要检查 y 是否能赋值给 x,编译器检查 x 中的每个属性,看是否能在 y 中也找到对应属性。 在这个例子中,y 必须包含名字是name 的 string 类型成员。y 满足条件,因此赋值正确。
检查函数参数时使用相同的规则:
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK
注意,y 有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容。
(多的 赋给 少的)
这个比较过程是递归进行的,检查每个成员及子成员。
报错提示(name 丢失)
比较两个函数
参数
相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:
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];
// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));
(少的 赋给 多的)
返回值
下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error because x() lacks a location property
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些 undefinded。
有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
被赋值的函数中使用:...args: any[]) => void
剩余参数时,赋值的参数可用任意多个参数传递,但类型需要与剩余参数对应
小示例
const getSum = (arr: number[], callback: (...args: number[]) => number): number => {
return callback(...arr)
}
const res = getSum([1, 2, 3], (...args:number[]): number => args.reduce((a, b) => a + b, 0))
console.log(res);
const res2 = getSum([1, 2, 3],(arg1: number, arg2: number, arg3: number): number => arg1 + arg2 +arg3)
console.log(res2);
函数参数双向协变
函数 A、B 中有相同的参数及类型, A、B 可相互赋值
let funA = (arg: number | string): void => {}
let funB = (arg: number): void => {}
// 函数 A、B 中有相同的参数及类型, A、B 可相互赋值
funA = funB
funB = funA
详细介绍: TypeScript中文手册.
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
类
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员 和 构造函数 不在比较的范围内。
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
类的私有成员
私有成员会影响兼容性判断(private
、protected
)。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
上面代码里,x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error, x and y are not compatible
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
let identity = function<T>(x: T): T {
// ...
}
let reverse = function<U>(y: U): U {
// ...
}
identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
引用来源: TypeScript 中文手册.