编者按:本文作者奇舞团前端开发工程师陈方旭。
本文是从前几天整理的《TypeScript 基础精粹》一文中抽出来的两部分:类型检查机制和类型保护机制,因为原文实在是太长太干了,如果有兴趣的同学可以点击文末的“阅读原文“查看全文。
let a = 1 // 推断为 number
let b = [ 1 ] // 推断为 number[]
let c = ( x = 1 ) = > x + 1 // 推断为 (x?: number) => number
当需要从多个类型中推断出一个类型的时候,TypeScript 会尽可能的推断出一个兼容当前所有类型的通用类型。
let d = [1, null]
// 推断为一个最兼容的类型,所以推断为(number | null)[]
以上的推断都是从右向左,即根据表达式推断,上下文类型推断是从左向右,通常会发生在事件处理中。
在确定自己比 TS 更准确的知道类型时,可以使用类型断言来绕过 TS 的检查,改造旧代码很有效,但是要防止滥用。
interface Bar {
bar : number
}
let foo = { } as Bar
foo . bar = 1
// 但是推荐变量声明时就要指定类型
let foo1 : Bar = {
bar : 1
}
X兼容Y:X(目标类型) = Y(源类型)
let s: string = 'a'
s = 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 // 不兼容
type Handler = (a: number, b: number) => void
function test ( handler : Handler ) {
return handler
}
1、参数个数
固定参数
Handler 目标函数,传入 test 的 参数函数 就是源函数。
let handler1 = (a: number) => { }
test ( handler1 ) // 传入的函数能接收一个参数,且参数是number,是兼容的
let handler2 = ( a : number , b : number , c : number ) = > { }
test ( handler2 )
可选参数和剩余参数
let a1 = (p1: number, p2: number) => { }
let b1 = ( p1 ? : number , p2 ? : number ) = > { }
let c1 = ( ... args : number [ ] ) = > { }
(1) 固定参数是可以兼容可选参数和剩余参数的。
a1 = b1 // 兼容
a1 = c1 // 兼容
(2) 可选参数是不兼容固定参数和剩余参数的,但是可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
b1 = a1 //不兼容
b1 = c1 // 不兼容
(3) 剩余参数可以兼容固定参数和可选参数。
c1 = a1 // 兼容
c1 = b1 // 兼容
2、参数类型
基础类型
// 接上面的test函数
let handler3 = ( a : string ) = > { }
test ( handler3 ) // 类型不兼容
接口类型
接口成员多的兼容成员少的,也 可以理解把接口展开,参数多的兼容参数少的 。对于不兼容的,也可以通过设置"strictFunctionTypes": false来消除报错,实现兼容。
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 // 不兼容
3、返回值类型 目标函数的返回值类型必须与源函数的返回值类型相同,或者是其子类型。
let f = () => ({ name: 'Alice' })
let g = ( ) = > ( { name : 'A' , location : 'beijing' } )
f = g // 兼容
g = f // 不兼容
4、函数重载 函数重载列表(目标函数)
function overload(a: number, b: number): number;
function overload ( a : string , b : string ) : string ;
函数的具体实现(源函数)
function overload(a: any, b: any): any { }
目标函数的参数要多于源函数的参数才能兼容
function overload(a:any,b:any,c:any):any {}
// 不兼容, 具体实现时的参数多于重载列表中匹配到的第一个定义的函数的参数,也就是源函数的参数多于目标函数的参数
返回值类型不兼容
function overload(a:any,b:any) {}
// 去掉了返回值的any,不兼容
enum Fruit { Apple, Banana }
enum Color { Red , Yello }
枚举类型和数字类型是完全兼容的
let fruit: Fruit.Apple = 4
let no : number = Fruit . Apple
枚举类型之间是完全不兼容的
let color: Color.Red = Fruit.Apple // 不兼容
和接口比较相似,只比较结构,需要注意,在比较两个类是否兼容时, 静态成员和构造函数是不参与比较的 ,如果两个类具有相同的实例成员,那么他们的实例就相互兼容。
class A {
constructor ( p : number , q : number ) { }
id : number = 1
}
class B {
static s = 1
constructor ( p : number ) { }
id : number = 2
}
let aa = new A ( 1 , 2 )
let bb = new B ( 1 )
// 两个实例完全兼容,静态成员和构造函数是不比较的
aa = bb
bb = aa
私有属性
类中存在私有属性情况有两种,如果其中一个类有私有属性,另一个没有。没有的可以兼容有的,如果两个类都有,那两个类都不兼容。
如果一个类中有私有属性,另一个类继承了这个类,那么这两个类就是兼容的。
class A {
constructor ( p : number , q : number ) { }
id : number = 1
private name : string = ''
}
class B {
static s = 1
constructor ( p : number ) { }
id : number = 2
}
let aa = new A ( 1 , 2 )
let bb = new B ( 1 )
aa = bb // 不兼容
bb = aa // 兼容
// A中有私有属性,C继承A后,aa和cc是相互兼容的
class C extends A { }
let cc = new C ( 1 , 2 )
// 两个类的实例是兼容的
aa = cc
cc = aa
泛型接口 泛型接口为空时,泛型指定不同的类型,也是兼容的。
interface Empty<T> {}
let obj1 : Empty < number > = { }
let obj2 : Empty < string > = { }
// 兼容
obj1 = obj2
obj2 = obj1
如果泛型接口中有一个接口成员时,类型不同就不兼容了。
interface Empty<T> {
value : T
}
let obj1 : Empty < number > = { }
let obj2 : Empty < string > = { }
// 报错,都不兼容
obj1 = obj2
obj2 = obj1
泛型函数 两个泛型函数如果定义相同,没有指定类型参数的话也是相互兼容的。
let log1 = <T>(x: T): T => {
return x
}
let log2 = < U > ( y : U ) : U = > {
return y
}
log1 = log2
log2 = log1
结构之间兼容:成员少的兼容成员多的;
函数之间兼容:参数多的兼容参数少的。
前置代码,之后的代码在此基础运行:
enum Type { Strong, Week }
class Java {
helloJava ( ) {
console . log ( 'hello Java' )
}
java : any
}
class JavaScript {
helloJavaScript ( ) {
console . log ( 'hello JavaScript' )
}
javaScript : any
}
实现 getLanguage 方法直接用 lang.helloJava 是不是存在作为判断是会报错的。
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
if ( lang . helloJava ) {
lang . helloJava ( )
} else {
lang . helloJavaScript ( )
}
return lang
}
利用之前的知识可以使用类型断言解决。
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
// 这里就需要用类型断言来告诉TS当前lang实例要是什么类型的
if ( ( lang as Java ) . helloJava ) {
( lang as Java ) . helloJava ( )
} else {
( lang as JavaScript ) . helloJavaScript ( )
}
return lang
}
类型保护第一种方法,instanceof 。
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
// instanceof 可以判断实例是属于哪个类,这样TS就能判断了。
if ( lang instanceof Java ) {
lang . helloJava ( )
} else {
lang . helloJavaScript ( )
}
return lang
}
类型保护第二种方法, in 可以判断某个属性是不是属于某个对象。
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
if ( 'java' in lang ) {
lang . helloJava ( )
} else {
lang . helloJavaScript ( )
}
return lang
}
类型保护第三种方法, typeof 类型保护,可以帮助我们判断基本类型。
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
// x也是联合类型,typeof类型保护,可以判断出基本类型。
if ( typeof x === 'string' ) {
x . length
} else {
x . toFixed ( 2 )
}
return lang
}
类型保护第四种方法,通过创建一个类型保护函数来判断对象的类型。
类型保护函数的返回值有点不同,用到了 is ,叫做 类型谓词。
function isJava(lang: Java | JavaScript): lang is Java {
return ( lang as Java ) . helloJava !== undefined
}
function getLanguage(type: Type, x: string | number) {
let lang = type === Type . Strong ? new Java ( ) : new JavaScript ( )
// 通过创建一个类型保护函数来判断对象的类型
if ( isJava ( lang ) ) {
lang . helloJava ( )
} else {
lang . helloJavaScript ( )
}
return lang
}
不同的判断方法有不同的使用场景:
typeof:判断一个变量的类型(多用于基本类型);
instanceof:判断一个实例是否属于某个类;
in:判断一个属性是否属于某个对象;
类型保护函数:某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内。
《奇舞周刊》是360公司专业前端团队「 奇舞团 」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。