所谓类型检查机制,就是编程语言编译器在做类型检查时,所秉持的原则,以及表现出的行为。
TS作为一门灵活的强类型语言:如果你声明一个变量,不一定都要做类型注解,ts会根据某些规则,自动推断出变量的类型。
先来看基础类型的推断:
let name1:string;
let name2 = ''; // 推断为string
let name // 推断为any
let num = 1 // 推断为number
let arr = [] // 推断为any[]
let arr2 = [1] //推断为 number[]
// 当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。
let arr3 = [1,null] // 推断为 number|null
在函数中也是如此:
// 自动推断入参和返回值都是number
const c = (x = 1) => x + 1
以上都是等号右边向等号左边的推断,也存在等号左边向右边的推断(上下文推断):
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button) //<- 报错
}
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.button) //<- 可以直接使用mousedown的专属属性。
}
有时候,你对你的代码有充足的自信,且想要推翻对ts的推论。这时候需要用到类型断言——回想鸭子模型,如果一个动物不符合鸭子的特征,那么开发者可以“断言”,让它被归类为鸭子。此处再复习下:
// 试改造如下代码
let foo = {}
foo.bar = 1 // <-报错
思路是定义一个接口,让它有bar这个属性:
interface Foo {
bar: number
}
let foo = {} as Foo // <- 不报错
// foo.bar = 1
到这里为止,代码不报错,相当于使用类型断言绕过了代码推断。但这个写法并不好。如果我不写foo.bar=1
,这里的检查就漏过去了。建议是直接注解foo:
let foo: Foo = {
bar: 1,
}
所以,不要滥用断言。
当一个类型Y可以被赋值给另一个类型Y时,我们就可以说,X兼容Y。
X兼容Y:X(目标类型)= Y(源类型)
举个例子,当tsconfig.json
中"strictNullChecks": false
时,以下操作时被允许的:
let s: string = "a"
s = null
为什么呢?因为在typescript中,null被默认为字符串的子类型。因此可以说:字符串类型兼容null类型。
再来看一个问题:
interface X {
a: any
b: any
}
interface Y {
a: any
b: any
c: any
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
x = y // <-正常
y = x // <-报错:类型 "X" 中缺少属性 "c",但类型 "Y" 中需要该属性。
让我们重新温故下鸭子模型:
" 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 "——在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。
只要Y接口具备X接口的所有必要成员,那么X就兼容Y(成员少的兼容成员多的)。
对于函数的兼容,包含以下条件:
1.参数的个数
// 声明一个Handler类型
type Handler = (a: number, b: number) => void
// 高阶函数
const hoc = (handler: Handler) => handler
// 1.参数个数<=Handler参数
let handler1 = (a: number) => {}
hoc(handler1)
// 参数个数>Handler参数
let handler2 = (a: number, b: number, c: number) => {}
// hoc(handler2) //<-报错
// 可选参数和剩余参数
let a = (p1: number, p2: number) => {}
let b = (p1?: number, p2?: number) => {}
let c = (...args: number[]) => {}
// 固定参数:可以兼容可选参数和不定参数
a = b
a = c
// 可选参数
// b = a // <- 报错,可配置tsconfig去除
// b = c // <- 报错,可配置tsconfig去除
// 不定参数
c = a
c = b
如果通过接口来定义两个函数的入参:
interface Point3D {
x: number
y: number
z: number
}
interface Point2D {
x: number
y: number
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}
p3d = p2d
p2d = p3d // <-报错
在此,对于定义了接口类型的函数参数个数多的兼容参数个数少的。
1.参数类型匹配
如果两个函数参数类型无法对应,二者无法兼容
// 声明一个Handler类型
type Handler = (a: number, b: number) => void
// 高阶函数
const hoc = (handler: Handler) => handler
let handler3 = (a: string) => {}
hoc(handler3) // <-报错:handler3 和 Handler不兼容
1.返回值类型:
目标函数的返回值类型必须与源函数的返回值相同,或为其子类型。
let f = () => ({ name: "djtao" })
let g = () => ({ name: "djtao", job: "coder" })
f = g
g = f //<-报错
1.函数重载
具体实现的函数中,参数不得多于重载签名,参数类型,返回类型只能是重载签名的类型。
// 重载列表
function overload(a: number, b: number): number
function overload(a: string, b: string): string
function overload(a: any, b: any): any {}
枚举和number可以相互兼容。
enum Fruit {
Apple,
Banana,
}
let fruit: Fruit.Apple = 1
let n_fruit: number = Fruit.Apple
但是枚举类型之间完全不兼容
let color: Color.Red = Fruit.Apple // <-报错
现有两个类A和B:
class A {
id: number = 1
constructor(p: number, q: number) {}
}
class B {
static s = 1
id: number = 2
constructor(p: number) {}
}
let aa = new A(1, 2)
let bb = new B(2)
aa = bb
bb = aa
比较两个类是否兼容:静态成员和构造函数是不参与比较的。在此基础上,如果拥有相同的实例成员(在上面例子中,相同实例成员为id),那么二者可以相互兼容。(此情形包括父类和子类之间)
两个类,公共成员一致,如果有私有成员(private):只需要考虑三种情况:
•一个有,一个没有:没有的兼容有的•子类有特殊的私有属性:父类兼容子类。•其它情况:相互不兼容
对于泛型接口,如果不定义任何成员,哪怕具体传参不同,都是相互兼容:
interface Empty<T> {}
let obj1: Empty<number> = {}
let obj2: Empty<string> = {}
obj1 = obj2
obj2 = obj1
但如果我在Empty中定义了一个成员:
interface Empty<T> {
value:<T>
}
二者互不兼容。也就是说:当成员类型被定义了,泛型接口之间就不能兼容。
对于泛型函数:如果两个泛型函数的定义相同,没有指定参数类型。那么两个函数之间是完全兼容的。
let log1 = <T>(x: T): T => {
console.log("x")
return x
}
let log2 = <U>(x: U): U => {
console.log("y")
return x
}
log1 = log2
log2 = log1
类型兼容性的用例非常多且繁杂,但是都是基于鸭子模型。为此,总结的规律是:
•结构之间:成员少的兼容成员多的•函数之间:参数多的兼容成员少的
先看个例子:
我们用枚举类型实现一个语言选择方法,逻辑是判断是否强类型,是则执行helloJava并返回Java,否则执行helloJavascript并返回JavaScript。目前已完成的代码如下:
enum Type {
Strong,
Weak,
}
class Java {
helloJava() {
console.log("hello java")
}
}
class JavaScript {
helloJavaScript() {
console.log("hello JavaScript")
}
}
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
return lang
}
getLanguage(Type.Strong)
接下来完成核心的getLanguage:
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
// 报错:类型“Java | JavaScript”上不存在属性...
if (lang.helloJava) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}
从报错信息看,ts是把lang作为了一种联合类型,以至于访问helloJava出错。所以我们使用类型断言:
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
// 注意,不能写`(lang as Java).helloJava`,3.7之后会校验
if (!!(lang as Java).helloJava) {
;(lang as Java).helloJava()
} else {
;(lang as JavaScript).helloJavaScript()
}
return lang
}
getLanguage(Type.Strong)
这段lang as
的断言写的有些非主流了,为了正常调用,不得不多次进行断言。而类型保护机制就是为了解决这类问题而诞生的。ts能够在特定的区块中保证变量属于某种确定的类型,你可以在此区块中放心使用此类型的使用和方法。
以下阐述四种创建此区块的方法。
Instanceof可以判断一个对象是否属于某种类型的实例。
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
// 1.instanceof
if (lang instanceof Java) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}
逻辑和instanceof类似,判断某个属性/方法是否存在。
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
// 2.in关键字
if ("helloJava" in lang) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}
// 3. typeof
if (typeof x == "string") {
// x可以直接获得字符串方法
}
// 4.类型保护函数
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined
}
const getLanguage = (type: Type) => {
let lang = type === Type.Strong ? new Java() : new JavaScript()
if (isJava(lang)) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}