类型兼容性
结构化类型赋值,与
结构化类型
相对应的名称是名义类型
,在 TS 中使用的是结构化类型
对象的赋值
interface Name {
name: string;
}
let x: Name
let y = { name: "zs", location: "上海" }
// OK
x = y
// error Property 'location' is missing in type 'Name' but required in type '{ name: string; location: string; }'
y = x
x = y
这样赋值是可以的,为什么呢?
这里有这么一个编译的过程,检查y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
反之,y = x
不成立的原因是:在检查x
能否赋值给y
的时候,y
需要有两个属性name
和location
,但是x
中只有一个name
属性,因此就不满足条件了。
总结:属性多的对象可以赋值给属性少的对象(前提是属性多的那个对象中包含有属性少的对象的那些属性)
函数的赋值
let x = (a: number) => 0
let y = (b: number, s: string) => 0
y = x // OK
x = y // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。ts(2322)
函数的比较是通过比较两个函数的参数列表
以及函数的返回值
。
上面的情况是参数不一致,返回值都是number
类型的函数,这里我就只要比较参数列表就行了。
y = x
是 OK 的,x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x
的每个参数在y
中都能找到对应的参数,所以允许赋值。
x = y
报错了,是因为y
有两个必须的参数,但是x
只有一个参数,因此就报错了。
这其实也挺好理解的,我们平时在使用某些函数的时候也会省略它的几个参数,而只是用我们需要的参数
// 完整的参数列表
;[1, 2, 3, 4]
.forEach((item, index, arr) => console.log(item))(
// 我们只需要使用第一个参数就行,因此省略了后面两个参数
[1, 2, 3, 4]
)
.forEach((item) => console.log(item))
总结:函数赋值的时候,参数少的可以赋值给参数多的,多余的参数相当于没有传参,值为(undefined),参数多的不可以赋值给参数少的,因为多余的参数都是必须的,不能这样省略。(前提是参数列表中有对应类型的属性,名字不同无所谓)
修改一下函数x
的返回值,返回一个string
类型的值
let x = (a: number) => "0"
这样一改之后,上面的两个赋值都不成立了,因为返回值类型不一致不能将类型“string”分配给类型“number”。ts(2322)
let x = () => ({ name: "zs" })
let y = () => ({ name: "ls", location: "上海" })
x = y // OK
y = x // error Property 'location' is missing in type '{ name: string; }' but required in type '{ name: string; location: string; }'.ts(2322)
总结:函数的返回值不同时,无法互相赋值,如果返回的是一个对象的时候,那么就需要再次判断两个对象能否赋值那个原则。
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
我们来修改一下上面的那个例子
interface yy {
name: string
location?: string
}
let res: yy = { name: "ls", location: "上海" }
let x = () => ({ name: "zs" })
let y = () => res
x = y // OK
y = x // OK
这里我们把location
变为了一个可选参数,这样之后两个的赋值都是允许的,因为 location 现在是一个可选参数,也就是说可以没有。
对于剩余参数来说,就相当于有无数个可选参数。把上面的函数进行修改如下,any
就可以接收任意类型的剩余参数了
let x = (a: number, ...rest: any[]) => 0
let y = (b: number, c: string) => 0
y = x // OK
x = y // OK
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
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<T> {}
let x: Empty<number>
let y: Empty<string>
x = y // OK, because y matches structure of x
上面代码里,x
和y
是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty<T> {
data: T
}
let x: NotEmpty<number>
let y: NotEmpty<string>
x = y // Error, because x and y are not compatible
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
let identity = function <T>(x: T): T {
// ...
}
let reverse = function <U>(y: U): U {
// ...
}
identity = reverse // OK, because (x: any) => any matches (y: any) => any