概述
Typescript 的类型兼容指不同的类型的变量可以相互替换和给编译器额外信息来识别具体类型,主要包括:
- 变量类型兼容
- 函数类型兼容
- 类型守卫
兼容
变量类型兼容
Typescript 的类型兼容不是依据类型名称,而是类型的成员。也就是说,两个类型如果成员相同或是其中一个类型只缺少可选类型,那么这两个类型就是兼容的。比如:
interface A {
name: string
value?: number
}
interface B {
name: string
}
let a: A = { name: 'b' } as B
let c: stirng = 0 // 错误
因为类型A
和B
的必需属性都是name
,所以B
类型变量可以赋值给A
类型变量。但是string
和number
不兼容,无法互相赋值。
在看一个例子:
function log(x: { name: string }) {}
let a = {
name: 'a',
value: 0
}
log(a)
参数x
要求对象有一个string
类型的name
成员,虽然对象a
多了一个value
成员,但是只要它有string
类型的name
成员,就满足兼容条件。
但有一个例外情况,如果给函数传入一个直接量,那么必须与参数的对象成员一致,不能有多余的成员,返回值同样如此。比如:
log({ name: 'a', value: 0 }) // 错误。value多余
类实例
不同类实例之间,只要实例成员兼容则类型兼容,不要求静态成员兼容。
示例:
class A {
static value: string = 'a'
log(x: string): void {}
}
class B {
static value: number = 0
log(x: string): void {}
}
let a: A = new B()
函数类型兼容
函数类型兼容主要是指函数的参数数量类型和返回值类型的兼容。
示例:
type Scale = (x: string, y: string) => { length: number }
let scale: Scale = function (x: string): { length: number } {
let result = { length: x.length }
return result
}
let v = scale('x', 'y')
函数参数和返回值也遵循之前的变量类型的兼容,除此外它还有更宽泛的兼容性,且参数和返回值的兼容方向不同。示例:
type Scale = (x: string, y: string) => { length: number }
let scale: Scale = function (x: string): { length: number } {
let result = { length: x.length }
return result
}
let v = scale('x', 'y')
Scale
类型函数的参数有两个,但是变量scale
被赋予只有一个参数的函数值,但这是合法的。参数是从函数外流向函数内,Scale
类型对外要求两个参数,但是这两个参数在函数体内并不一定需要全部被用到。因为函数体即使只使用了一个参数,另一个参数没有使用,也不会发生运行时错误,所以这样的类型是兼容的。也就是说,函数变量的参数只要类型兼容且数量小于等于类型声明的参数数量,两者就是兼容的。
再来看返回值类型不同的情况:
type Scale = (x: string) => { length: number }
let scale: Scale = function (x: string): { length: number } {
let result = { length: x.length, value: x }
return result
}
let v = scale('x')
Scale
类型函数的返回值只有一个length
成员,但是变量scale
的返回值有length
和value
两个成员,这也是合法的。返回值从函数内流向函数外,Scale
类型对外宣称返回的对象有一个成员,那么只要在满足外部期望,即返回了length
成员的情况下,就是合法的。置于额外的成员是否被用到,是编译器无法推断的。
总结来说,就是数据流出方满足数据流入方的期望,不会造成出现undefined
的运行时错误,类型就是兼容的。
类型守卫
null 和 undefined
有些变量用联合类型使得它们可以为空(null
或undefined
),比如:
function scale(x: string | null) {
return x.length // 错误
}
因为x
可能为null
,但是null
没有length
成员,所以编译器报错。可以通过判断x
是否存在来兼容:
function scale(x: string | null) {
if (x) {
// 或 x !== null
return x.length
} else {
return 0
}
}
有时我们可以确认一些可控类型的变量一定有值,那么我们可以使用操作符!
来告诉编译器这个变量不可能为null
或undefined
。比如:
let a = document.getElementById('a')
let b = a.tagName // 错误
let c = a!.tagName
因为变量a
可能为null
所以不能直接读取tagName
属性,如果我们能确定这个元素存在,我们可以在变量a
后加上!
,编译器就认为a
一定有值,不再报错。
多类型函数参数
有些函数的参数类型是一个联合类型,可以传入多种类型的参数,但在使用参数时不一定兼容。
function scale(x: string | number) {
return x.length // 错误
}
因为number
类型没有length
成员,所以 Typescript 会报告这里可能会有错误。我们可以使用typeof
来判断这里的类型,比如:
function scale(x: string | number) {
if (typeof x === 'string') {
return x.length
} else {
return x.toString().length
}
}
Typescript 编译器会检测到代码判断了类型,所以这里读取length
属性是安全的。
也可以使用instanceof
判断原型。示例:
function value(x: Date | string) {
if (x instanceof Date) {
return x.toUTCString()
} else {
return x.toUpperCase()
}
}
成员名
通过操作符in
判断成员是否存在,进而判断类型。
示例:
interface A {
name: string
}
interface B {
value: number
}
function scale(x: A | B) {
if ('name' in x) {
return x.name.length
} else {
return 0
}
}
接口字面量成员
对于有字面量类型成员的类型,可以通过判断字面量成员来确定类型。
示例:
interface A {
name: 'a'
value: string
}
interface B {
name: 'b'
value: number
}
function scale(x: A | B) {
if (x.name === 'a') {
return x.value.length
} else {
return 0
}
}
因为接口A
的成员name
只能被赋值为a
,所以在函数中如果判断参数x
的成员name === 'a'
,就能推断x
是A
类型,那么其value
属性是字符串,可以读取length
成员。
自定义类型守卫
如果上述的方法不够用,我们可以自定义类型守卫,就是编写一个判断函数。示例:
interface A {
name: 'a'
value: string
}
interface B {
name: 'b'
value: number
}
function isA(x: any): x is A {
return x.name === 'a'
}
function scale(x: A | B) {
if (isA(x)) {
return x.value.length
} else {
return 0
}
}
我们通过在函数isA
的返回值处用操作符is
来声明这是一个自定义的类型守卫,且这个守卫的用处就是用来判断参数x
是否是A
类型。函数体内是进行判断的具体逻辑,函数的返回值必须是boolean
。这个函数的意义就是由我们来告诉编译器怎么判断一个参数是否是A
类型,可以用在判断语句(比如if
)中,这样编译器对相应的变量可以进行类型推断。
在类中可以使用this
作为is
的操作数。示例:
class C implements A {
name: 'a' = 'a'
value: string = '0'
isA(): this is A {
return this.name === 'a'
}
}