TypeScript 的类型检查和类型保护

编者按:本文作者奇舞团前端开发工程师陈方旭。

本文是从前几天整理的《TypeScript 基础精粹》一文中抽出来的两部分:类型检查机制和类型保护机制,因为原文实在是太长太干了,如果有兴趣的同学可以点击文末的“阅读原文“查看全文。

类型检查机制

类型检查机制:TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。其作用是辅助开发,提高开发效率。

类型推断

类型推断: 指的是不需要指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动地为其推断出一个类型。

基础类型推断
 
  

let a = 1 // 推断为 number

let b = [1] // 推断为 number[]

let c = (= 1) => x + 1 // 推断为 (x?: number) => number


最佳通用类型推断

当需要从多个类型中推断出一个类型的时候,TypeScript 会尽可能的推断出一个兼容当前所有类型的通用类型。

 
  

let d = [1, null]

// 推断为一个最兼容的类型,所以推断为(number | null)[]

// 当关闭"strictNullChecks"配置项时,null是number的子类型,所以推断为number[]


上下文类型推断

以上的推断都是从右向左,即根据表达式推断,上下文类型推断是从左向右,通常会发生在事件处理中。

类型断言

在确定自己比 TS 更准确的知道类型时,可以使用类型断言来绕过 TS 的检查,改造旧代码很有效,但是要防止滥用。

 
  

interface Bar {

  bar: number

}

let foo = {} as Bar

foo.bar = 1


// 但是推荐变量声明时就要指定类型

let foo1: Bar = {

  bar: 1

}

类型兼容

当一个类型Y可以被赋值给另一个类型X时,我们就可以说类型X兼容类型Y。

X兼容Y:X(目标类型) = Y(源类型)

 
  

let s: string = 'a'

= null // 把编译配置中的strictNullChecks设置成false,字符类型是兼容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 }

// 源类型只要具有目标类型的必要属性,就可以进行赋值。接口之间相互兼容,成员少的兼容成员多的。

= 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) // 会报错 传入的函数能接收三个参数(参数多了),且参数是number,是不兼容的

可选参数和剩余参数

 
  

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' })

= 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 = '' // 只在A类中加这个私有属性,aa不兼容bb,但是bb兼容aa,如果A、B两个类中都加了私有属性,那么都不兼容

}

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


兼容性总结
  • 结构之间兼容:成员少的兼容成员多的;

  • 函数之间兼容:参数多的兼容参数少的。

类型保护机制

类型保护机制:指的是 TypeScript 能够在特定的区块(类型保护区块)中保证变量属于某种特定的类型。可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。

前置代码,之后的代码在此基础运行:

 
  

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()


  // 如果想根据lang实例的类型,直接用lang.helloJava是不是存在来作为判断是会报错的,因为现在lang是Java和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()


  // in 可以判断某个属性是不是属于某个对象 如上helloJava和java都能判断出来

  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公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

640?wx_fmt=png


TypeScript中,有几种方法可以用来判断类型。其中一种方法是使用typeof操作符。通过typeof v === "typename"或typeof v !== "typename"的形式,可以判断一个变量的类型是否为"number","string","boolean"或"symbol"等。但是需要注意的是,TypeScript并不会阻止你与其他字符串进行比较,这些表达式不会被识别为类型保护。\[1\] 另一种方法是使用instanceof关键字。通过使用instanceof关键字,可以判断一个对象是否属于某个类或接口的实例。例如,在一个泛型函数中,可以使用extends关键字约束泛型类型T必须是某个对象类型的子类型,然后使用keyof操作符获取T类型的所有键,最后使用extends关键字约束K类型必须是keyof T联合类型的子类型。这样就可以在函数中使用obj\[key\]来获取对象的属性值。\[2\] 此外,TypeScript还提供了联合类型的概念。联合类型允许一个变量具有多种可能的类型。例如,可以使用string | boolean来声明一个变量,该变量可以是字符串类型或布尔类型。\[3\] 综上所述,TypeScript提供了多种方法来判断类型,包括typeof操作符、instanceof关键字和联合类型。这些方法可以根据具体的需求来选择使用。 #### 引用[.reference_title] - *1* *3* [TypeScript 类型检查总结](https://blog.csdn.net/m0_45406092/article/details/106854643)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [typescript类型判断](https://blog.csdn.net/meng16/article/details/111404848)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值