TypeScript学习 --- TS中的类型(1)

340 阅读13分钟

TS是JS的超集,所以TS支持JS原生提供的数据类型

并在此基础上进行了扩展

number

和JS一样,在TS中,number是不区分整数(int)或浮点数(float或double)

只有一个number类型,统一按照浮点数的方式来进行存储

let num: number

/* 
  tips: num: number 和 num: Number 是不一样的
    	number -> 数值类型 Number -> number类型所对应的包装类类型
*/

// 在TS中,和js一样,number执行十进制,八进制,二进制和十六进制
num = 100 // 十进制
console.log(num) // => 100

// 进制中的值不区分大小写,也就意味着 ob100 <=> oB100 ... 推荐使用小写方式
num = 0b100 // 二进制  0b100 <=> 0B100
console.log(num) // => 4

num = 0o100 // 八进制 0o100 <=> 0O100
console.log(num) // => 64

num = 0x100 // 十六进制 0x100 <=> 0X100
console.log(num) // => 256

boolean

let bool: boolean

// 布尔类型就只有2个值,分别为true或者false
bool = true
bool = false

// 还有一种比较特殊的赋值方式
bool = 20 > 30

string

// 字符串支持单引号,双引号和模板字符串
let firstName: string = 'Klaus'
let lastName: string = "Wang"

// tips: 默认情况下,如果变量的类型是可以被推导(infer)出的
// 那么我们在实际使用的时候,一般会省略该变量的类型注解(type annotation)
let fullName: string = `${firstName} - ${lastName}`

// 一般写成
// let fullName = `${firstName} - ${lastName}`

array

// 这就是在ts中 arr的数据类型为数组
let arr = []

但是这样是不推荐的,在JS中数组可以存放不同的数据类型,这其实是非常不好的一种编码方式

所以在ts中定义一个变量的数据类型为数组的时候,需要决定2个部分的内容

1、 这个数据类型是一个数组

2、数组中存放的每一个数据项是什么类型的元素

let arr = [] // => 没有指定每一个数据项是什么类型的前提条件下
// arr的类型注解会被推断Wie any[]

arr.push(123) // success
arr.push('str') // success

所以实际在定义一个变量为数组的时候,一帮采用以下的2种方式:

let arr1: string[] = [] // 类型[] 推荐
let arr2: Array<string> = [] //  Array<泛型> 不推荐 --- 和JSX语法冲突,无法在JSX中使用

object

// 可以使用object这个类型注解来表示info类型的值是object类型
let info: object = {
  username: 'Klaus',
  uid: 'A1080023'
}

但是不推荐在开发中使用object类型注解

因为ts需要先编译后成为js才可以交给浏览器去解析和执行

那么也就意味着ts校验的时候是只看类型不看值的,也就意味着如果使用object为类型注解,

ts在编译的时候,根本无法及时知道info中有哪些属性和方法,即info只能调用对象的公共属性和方法,无法调用自身上特有的属性和方法

let info: object = {
  username: 'Klaus',
  uid: 'A1080023'
}

// 可以调用对象的公共属性和方法
console.log(info.toString()) // => [object Object]

// 但是编译时ts无法知道info中有哪些自己的属性和方法,所以不推荐在实际使用中
// 给一个对象的类型注解设置为object

// console.log(info.uid) => error 类型“object”上不存在属性“uid”
// info.phone = '13122093345' => error 同样赋值也是会报错的

null 和 undefined

null 和 undefined 在ts中是2个非常特殊的值

// 1. 他们只有一个值,也就是他们自己本身
let n: null = null
let un: undefined = undefined
// 2. null 和 undefined 类型在定义的时候,最好需要加上类型注解,而非是使用类型推断
// 因为在TS中null 和 undefined 可以被认为是任何类型的子类型 
// 因此在这里进行类型推断的时候,n 和 un 的类型会被推断为 any
let n = null
let un = undefined

n = 'Klaus' // success
un = 'Klaus' // success
// 3. 虽然null 和 undefined 在ts中被认为是任意类型的子类型
// 但是默认条件下,下边的代码是会报错的
let str: string = null
let num: number = undefined

// 这是因为在ts在编译的时候,默认开启了严格检测(在严格检测中有一个子选项叫做checkNullChecks默认是开启的)
// 在这个选项开启的时候 null 和 undefined 只能赋值给自身,是不能赋值给其它类型数据的
// 只有当checkNullChecks关闭的时候,null 和 undefined 才会被允许赋值给任意类型的数据

symbol

// symbol --- 符号 --- 独一无二的值
// 可以使用Symbol函数来创建一个symbol值
// Symbol函数的参数只是一个标识符,仅仅只是起到标记的作用
let sym1: symbol = Symbol('symbol')
let sym2: symbol = Symbol('symbol')

console.log(sym1 === sym2) // => false

any 和 unknown

any 和 unknown 表示的都是 当前变量的数据类型不确定

区别在于

  1. any 表示的是 当前数据类型 不确定,设置为任意类型 (也就是没有类型,即这个变量不被ts的类型检测系统监控)
  2. unknown 表示的是当前数据类型不确定,但将当前变量的类型暂时设置为未知
// 如果将一个变量设置为any类型,
// 可以对any类型的变量进行任何的操作,包括获取不存在的属性、方法
// 可以一个any类型的变量赋值任何的值,比如数字、字符串的值;
let foo: any = 123
foo = 'Klaus'
foo = true

console.log(foo.length)
console.log(foo())

// 对于某些情况的处理过于繁琐不希望添加规定的类型注解,
// 或者在引入一些第三方库时,缺失了类型注解,这个时候 我们可以使用any
// let res // -> 如果一个变量没有赋值,只是进行了定义,那么这个变量的类型会被推断为any
let res: unknown // 一个变量如果在定义的时候,无法确定变量的数据类型的时候,可以定义这个变量的类型为unknown 而非any

function fun1() {
  return 123
}

function fun2() {
  return 'Klaus'
}

let flag = true

res = flag ? fun1() : fun2()

any 和 unknown 的区别是

  1. any类型的值可以赋值给其它任何的数据类型

  2. unknown类型的值只能赋值给any或unknown类型的变量

let foo: unknown
let foz: any

let num: number = foz // any -> number  --- success
// num = foo  //  unknown -> number --- error

let str: string = foz // any -> string --- success
// str = foo  //  unknown -> string --- error

Tips: any主要是用来解决在某些特定的情况下,无法很好的对js添加合适的类型注解的时候,可以暂时将这个变量的类型注解设置为any

也就是没有类型,不经过ts的类型检测,但是这种情况是十分少见的,所以不推荐在ts编写的代码中频繁使用any作为变量的类型标注

void

当一个函数没有返回值的时候,这个类型就是void类型

在js中,如果一个函数没有返回值,那么这个函数默认的返回值就是undefined

所以在ts中,null和undefined也是void的子类型,

也就是一个返回类型为void的函数,可以返回null 和 undefined

// 在给一个函数添加类型注解的时候,一般只给函数参数添加类型注解,函数返回值的类型注解一般是省略的,因为可以类型推断得出

// 这个函数什么都没有返回(或者说返回了undefined) 那么这个函数的返回类型就是void
function sum(num1: number, num2: number): void {
  console.log(num1 + num2)

  // 一个函数如果什么都不返回的时候,那么这个默认的返回值就是undefined
  // 所以undefined肯定是void的子类型
  return undefined // success
}
function sum(num1: number, num2: number): void {
  console.log(num1 + num2)

  // 默认开启strictNullChecks的时候, null是不可以作为void的子类型的
  return null // 但是关闭strictNullChecks后,null就可以在这里作为void函数的返回值
  // 因为此时 null 就可以作为void类型的子类型
}

never

Never表示永远不会发生值的类型

如果一个函数中是一个死循环或者抛出一个异常,那么这个函数的返回值类型是never

never类型的值只能赋值给never类型,never类型不是任何类型的子类型,包括any

function foo(): never {
  while(true) {

  }
}

function baz(): never {
  throw new Error('error message')
}
// never类型的应用场景

function handleMessage(message: number | string) {
  switch (typeof message) {
    case 'number':
      console.log('number 处理逻辑')
      break

    case 'string':
      console.log('string 处理逻辑')
      break
  }
}

// ------------------------------------------------
// 此时如果为参数message 添加一个新的类型boolean,但是不添加对应的处理逻辑
function handleMessage(message: number | string | boolean) {
  switch (typeof message) {
    case 'number':
      console.log('number 处理逻辑')
      break

    case 'string':
      console.log('string 处理逻辑')
      break
  }
}

// ts不会报错,因为message此时可以传递的数据类型中存在boolean类型
// 但是handleMessage这个函数中,没有对参数为boolean的时候进行逻辑处理
// 显然这个函数在实际执行的时候是没有意义的
handleMessage(true)

// ------------------------------------------------
// 因此我们可以对这个函数进行相应的修改

function handleMessage(message: number | string) {
  switch (typeof message) {
    case 'number':
      console.log('number 处理逻辑')
      break

    case 'string':
      console.log('string 处理逻辑')
      break

    default:
      // 如果之前所有的类型都处理完成了,那么也就不会进行default
      // 所以此时message的类型就是never
      // 但是如果此时有类型没有处理完全,如刚刚例子中的boolean类型没有处理的时候
      // message就是boolean的值,此时ts就会报错
      const foo: never = message
  }
}

tuple

元组(tuple)表示的是一组数据的集合,是一种和数组类似的数据结构

元组具有以下的类型特征:

  1. 元组的长度是固定的
  2. 元组各个位置上的数据项的数据类型都是确定的
let tuple: [number, string] = [123, 'Klaus']
// T是泛型 可以认为一个变量,变量接收了次调用者传入参数的数据类型
// 例如 useState(10) T ==> number   useState('Klaus')  T ==> string

function useState<T>(state: T): [T, (state: T) => void] {
  let currentState = state

  function updateState(state: T) {
    // state的更新逻辑 ......
  }

  // 对于返回的结果个数是固定的,且数量不是特别多的情况下,可以直接返回元组
  // 如果返回对象,那么调用者就必须知道返回的对象的每一个字段名后才可以进行解构
  // 如果此时返回的元组,那么调用者就只需要知道对位置是什么数据类型即可进行解构后调用
  return [state, updateState]
}

// count ==> number counter ===> (state: number) => void
const [count, counter] = useState(10);  

// title ==> string updateTitle ===> (state: string) => void
const [title, updateTitle] = useState('Klaus'); 
function useState<T>(state: T) {
  let currentState = state

  function updateState(state: T) {
    // state的更新逻辑 .....
  }

  return [state, updateState]
}

// 注意: 需要手动指定返回的数据类型是tuple类型
// 如果使用类型推断的时候,默认的返回值类型会被认定为数组
// 此时count和 counter的类型会一样 为 number | (state: number)=> void
const [count, counter] = useState(10);

function

// 可以给函数的参数和返回值设置类型标注
// 如果给参数设置了类型标注,在调用这个函数的时候,ts会对函数的参数的个数和数据类型进行检测
// 一般对于函数返回值的类型标注我们一般可以省略不写,因为ts会自动进行类型推断
function sum(num1: number, num2: number): number {
  return num1 + num2
}
// 如果一个函数是自定义函数,那么这个函数的参数需要加上类型注解,如果不加会被推导为any
function add(num1: number, num2: number) {
  return num1 + num2
}

const persons = ['Klaus', 'Alex', 'Steven']

// 如果一个函数是回调函数,那么ts会根据执行上下文自动推测出正确的数据类型
// 所以在这种情况下,函数参数的数据类型是可以省略的
persons.forEach(item => console.log(item))

对象类型

// {x: number, y: number} 表示的是一个对象类型
// 也就是传入的参数中有且必须要有x和y2个属性,且这2个属性的类型必须都为number
function printPoint(point: {x: number, y: number}) {
  console.log(point.x + ' ' + point.y)
}
/*
  在对象类型中,属性和属性之间的分割符号可以使用逗号,也可以使用分号, 也可以省略
  最后一个分割符号是可选的
  {x: number, y: number}
  {x: number, y: number,}
  {x: number; y: number}
  {x: number; y: number;}
  {x: number y: number}
*/
function printPoint(point: {x: number; y: number}) {
  console.log(point.x + ' ' + point.y)
}
// ? --- 可选参数
function printPoint(point: {x: number; y: number, z?: number}) {
  console.log(point.x + ' ' + point.y + ' ' + point.z)
}

union type

TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型

其中一种组合方式就是联合类型(Union Type)

1. 联合类型是由两个或者多个其他类型组成的类型;
2. 表示可以是这些类型中的任何一个值
3. 联合类型中的每一个类型被称之为联合成员(union's members)
//  number | string 就是一种联合类型
function printID(id: number | string) {
  console.log(id)
}
function printID(id: number | string) {
  if (typeof id === 'string') {
    // ts会根据上下文在合适的地方对我们的类型进行缩小(narrow)
    console.log(id.toUpperCase()) //  此时ts会认为执行到此处,id的类型一定为string
  } else {
    console.log(String(id).toUpperCase())
  }
}
// 可选操作符不一定需要添加在对象中,任意地方都可以使用
function printMessage(message?: string) {
  console.log(message)
}

/*
  可选操作符类似于一种特殊的联合类型 --- 值本身 | undefined
  区别在于 使用联合类型 如果不需要传递参数,那么需要显示的传递undefined
  但是如果是可选操作符, 可以不传递任何参数,不需要显示的传递undefined
*/
function printMsg(message: string | undefined) {
  console.log(message)
}

printMessage('Hello World')
printMessage()
printMessage(undefined) // undefined是任何类型的子类型 所以在关闭strictNullChecks的时候,传递null也是合法的

printMsg('Hello TypeScript')
printMsg(undefined)
// printMsg() // error 如果是联合类型 不可以不传递参数,需要显示的传递一个undefined

类型别名

如果我们一个类型注解比较复杂(例如: { x: number, y: number, z?: number})放在代码中会减低代码的可读性

如果一个我们自定义的类型需要多次调用的时候,为了避免书写重复的代码

我们可以给一个类型起别名(type alias)

// 使用type关键字定义类型别名
// 一般推荐别名的命名方式为 xxxType 或 Xxx(首字母大写) 以便于和变量进行区分
type pointType = {x: number, y: number, z?: number}

// 类型别名的使用
function printPoint(point: pointType) {
  console.log(point)
}

类型断言

TypeScript有时候无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions),将一个类型从宽泛的类型转换为更为具体的类型

TypeScript只允许类型断言转换为 更具体(父类转换为子类) 或者 不太具体 (转换为any或者unknown)的类型版本,此规则可防止不可能的强制转换

// 获取页面上的Img标签
const imgElem = document.getElementById('img')

// 如果使用默认推导的时候,imgElem会被推导为HTMLElement | null
// 所以imgElem只能调用父类HTMLElement中所有的属性和方法
if (imgElem) {
  imgElem.src = 'http://p2.music.126.net/s72pX_GQDfFB8cew8oU83g==/109951165519029869.jpg' // error 
}
// 这么写也是错误的,因为HTMLImageElement有HTMLElement没有的属性和方法,所以无法进行类型赋值
const imgElem: HTMLImageElement | null = document.getElementById('img')

if (imgElem) {
  imgElem.src = 'http://p2.music.126.net/s72pX_GQDfFB8cew8oU83g==/109951165519029869.jpg' // error 
}
// 手动告诉ts,imgElem的类型就是HTMLImageElement, 是HTMLElement的一个具体的子类型
const imgElem  = document.getElementById('img') as HTMLImageElement

if (imgElem) {
  imgElem.src = 'http://p2.music.126.net/s72pX_GQDfFB8cew8oU83g==/109951165519029869.jpg' // success
}
// 类型断言的2种形式
let imgElem  = <HTMLImageElement>document.getElementById('img') // 泛型 --- 不推荐 和 JSX冲突
imgElem  = document.getElementById('img') as HTMLImageElement // as关键字
const str = 'Kluas'

// 一个极端的例子,我需要将string类型转换为number类型
// const num: number = str as number // error 类型只能是相近类型之间才可以进行转换

// 使用any或者unknown作为中转类型
let num = str as any as number
num = str as unknown as number