Skip to content

类型声明

作者:Yuan Tang
更新于:5 个月前
字数统计:5.3k 字
阅读时长:20 分钟

类型含义

js

javascript 本身提供类型支持,但有以下几个问题。

  • js 是弱类型,类型可以根据值发生改变

    js
    let a = 1
    const b = 2
    let c = a + b
    
    // 改变值后,a 的类型是字符串,这是不稳定的
    a = 'tydumpling'
    c = a + b
  • 使用 js 不能自定义类型,比如下面的 name 我们想只允许是 tydumpling、data,但实际上是可以传递任何值的。

    js
    function getTitle(name) {
      return name === 'tydumpling' ? 'tydumpling' : 'data'
    }
    
    getTitle('TS') // 不报错

    如果换成 ts 就方便了,我们可以定义类型来修饰参数 name,使其值只能为 tydumpling 或 data

    js
    function getTitle(name: 'tydumpling' | 'data') {
      return name === 'tydumpling' ? 'tydumpling' : 'data'
    }
    
    getTitle('TS') // 报错

typescript

可以将 typescript 中的类型理解为一类值的集合,比如 'tydumpling'、'tydumpling'都属于 string 类型集合。

  • 有些类型没有值比如 never,不能将任何值赋予 never 类型的变量

  • 有些类型只有一个值,下面的fn变量的值只能是字符串a

    js
     type tydumpling = 'a'
    const fn: tydumpling = 'a'
  • string、number 是无限集合,而上例中的 tydumpling 类型为有限集合

类型校验

下面没有使用类型限制时,函数参数传入字符串也是可以执行的,显示这个结果是不对的

js
function sum(a, b) {
  return a + b
}

console.log(sum('a', 3)) // 结果为 a3

加上严格类型后,在编译环节就会提示错误

js
function sum(a: number, b: number) {
  return a + b
}

console.log(sum('a', 3))
// 报错 Argument of type 'string' is not assignable to parameter of type 'number'.

类型推断

当没有明确设置类型时,系统会根据值推断变量的类型

字符串

下例中系统会根据值推断 fn 变量为 string,当将 fn 设置为 18 的 number 类型时编译时将报误

js
let fn = 'tydumpling.com' // let fn: string
fn = 18 // 报错

数值

ts 中的数值类型包括了小数、负数、整数

js
let fn = 100 // let fn: number
fn = 100.1
fn = -101

布尔值

值为 true 或 false 会被推断为 boolean 类型

js
let state = true // let state: boolean
state = 1 > 2 // 比较运算符会返回布尔值,因此不会报错

数组

下面是数组类型的推断结果,表示数组内容值为字符串

js
const fn = ['tydumpling.com', 'tydumpling'] // const fn: string[]

fn.push(100) // 因为数组类型只能是字符串,不允许数字,所以报错

下面会推断数组允许的值为字符串或数值

js
const fn = ['tydumpling.com', 'tydumpling', 100] // const fn:(string|number)[]

fn.push(100, 'TS') // 数组允许数值、字符串类型,所以编译通过
fn.push(true) // 报错

对象

ts 也可以推断字面量类型

js
const user = { name: 'tydumpling', age: 18, open: true }

推断结果如下

js
const user: {
  name: string
  age: number
  open: boolean
}

如果向对象中添加类型中不存在的属性将报错

js
const user = { name: 'tydumpling', age: 18, open: true }

user.city = '北京'
// 将产生错误 Property 'city' does not exist on type

下面是更复杂的在对象中嵌套对象,TS 也是可以推断出来的

js
const user = {
  name: 'tydumpling',
  age: 18,
  open: true,
  lessons: [
    { title: 'linux' },
    { title: 'TS' }
  ]
}

user.lessons[0].title

上例推断的结果是

js
const user: {
  name: string
  age: number
  open: boolean
  lessons: {
    title: string
  }[]
}

配置文件

TS 支持对编译过程使用配置项自定义,因为下面要讲的有些类型在不同 TS 配置时有差异,所以我们要掌握 TS 配置文件的创建与使用。

初始化

配置项的具体选项使用,会在讲到某个知识点用到时再具体说

执行以下命令创建配置项

js
tsc --init

然后执行以下命令使用配置项的定义进行监测

js
tsc - w

也可以使用 vscode 终端 > 运行任务 >typescript菜单运行监视

配置选项

配置说明
noImplicitAny禁止使用隐含的 any 类型,如函数参数没有设置具体类型
strictNullChecks开启时不否允许将 null、undefined 赋值给其他类型比如字符串
target转换成 JS 的版本
strict是否严格模式执行
module使用的模块系统

基本类型

除了上面的类型自动推断外,更多的时候是明确设置变量类型

字符串

字符串使用 string 来声明

js
const fn: string = 'tydumpling.com'

数值

在 TS 中不区分整数与浮点数,都使用 number 来声明

js
const fn: number = 100

布尔

使用 boolean 来声明布尔类型

js
const fn: boolean = true

数组

下面是对数组值类型为字符串

js
const fn: string[] = []
fn.push('tydumpling', 'tydumpling')
fn.push(123) // 报错

也可以使用泛型来声明数组(泛型的详细内容请参考泛型)

js
const fn: Array<string> = []
fn.push('tydumpling', 'tydumpling')

创建值类型字符串的数组,并填充内容为tydumpling.com

js
const fn = [] < string > (3).fill('tydumpling.com')
console.log(fn) // ['tydumpling.com', 'tydumpling.com', 'tydumpling.com']

元组

明确数组每个成员值类型的数组为元组

js
let fn: [string, number, boolean]
fn = ['tydumpling.com', 2090, true]
console.log(fn) // ['tydumpling.com', 2090, true]

对象

下面是声明对象类型但不限制值类型

js
let fn: object
fn = { name: 'tydumpling' }
fn = {} // 使用字面量声明对象
fn = [] // 数组是对象
fn = Object.prototype // 原型对象
fn = 'tydumpling' // 报错,改变了类型为字符串

限定对象值类型

js
let fn: { name: string;year: number }

fn = { name: 'tydumpling', year: 2010 }

属性后面跟上? 用来指定 url 为可选值,这样的属性是非必填项

js
let fn: { name: string;year: number;url?: string }
fn = { name: 'tydumpling', year: 2010 }

如果希望能够自足添加任意类型的属性,可以写成如下形式:

ts
let fn: { name: string; [propName: string]: any }

这句代码表示创建该对象时除了 name 以外可以设置任意个键名为字符串型的属性,值可以为任意类型,如下:

ts
fn = { name: 'tydumpling', age: 2010 }

索引签名

如果有明确的索引名称可以使用下面方式来定义签名

js
interface tydumpling {
  name: string
  city: string
}

const fn: tydumpling = {
  name: 'tydumpling',
  city: 'beijing'
}

如果定义任意属性的签名,可以使用索引签名完成

js
interface tydumpling {
  [key: string]: keyof any
}

const fn: tydumpling = {
  name: 'tydumpling'
}

也可以添加明确的索引

js
interface tydumpling {
  [key: string]: keyof any
  city: string
}

const fn: tydumpling = {
  name: 'tydumpling',
  city: 'beijing'
}

下例中我们要求索引后有 fn 的后缀,则可以使用模板字面量形式

js
interface tydumpling {
  [key: `${string}fn`]: keyof any
}

const fn: tydumpling = {
  namefn: 'tydumpling'
}

当然也可以使用 Record 工具类型来定义

js
type tydumpling = Record<string, string>

const fn: tydumpling = {
  name: 'tydumpling'
}

Record 可以使用联合类型定义索引

js
type tydumpling = Record<'name' | 'age' | 'city', string>

const fn: tydumpling = {
  name: 'tydumpling',
  age: '18',
  city: 'beijing'
}

any

使用 any 指包含所有值的顶部类型,所以 any 不进行类型检查,等于关闭了 TS 对该变量的严格类型校验

  • 使用 any 类型等同于使用纯 JavaScript 的开发方式
  • any 类型是顶部类型,所有其他类型是他的子类型
  • 使用 any 类型将失去 typescript 静态类型的强制检测
  • 只有在描述一个根本不知道的类型时使用 any

可以将 any 视为所有类型的组合表示

js
let fn: string | boolean | number
fn = 'tydumpling'

let tydumpling: any
fn = 'tydumpling'

下面是设置基本 any 的示例

js
let fn: any

// 以下赋值不会报错
fn = 'tydumpling'
fn = 2010
fn = true
fn = []
fn = {}
fn = class {}

在数组中使用 any 类型,可以设置任意类型的值

js
const fn: any[] = ['tydumpling.com', 'tydumpling', 2010, true]

也可以使用泛型的方式设置 any 类型数组

js
const fn: Array<any> = ['tydumpling.com', 'tydumpling', 2010, true]

为对象属性设置类型

js
let fn: {
  name: any
  year: any
}
// 以下设置都不会报错
fn = { name: 'tydumpling', year: 2010 }
fn = { name: 2010, year: 'tydumpling' }

any 太过宽泛所以不建议使用,他会丢失 TS 的严格类型校验,比如下面的示例并不会报错

js
let fn: any
fn.get() // 不会报错

下面再来看一下对象的使用 any 类型造成的问题

js
class fn {
  constructor() { }
  get = () => 'tydumpling'
}

const obj: any = new fn()
console.log(obj.get())

obj.show()

所以上例需要指定正确的 fn 类型,而不是使用 any

js
...
const obj:fn = new fn;
...

能过设置 tsconfig.json 的 noImplicitAny=true 配置项,可以禁止隐含的 any 类型。以下代码会在编译时报错

js
function sum(a, b) {
  return a + b
}

unknown

unknown 类型也是顶部类型这与 any 一样

  • unknown 用于表示未知的类型
  • 会进行 TS 的类型检查,any 不进行 TS 检查
  • 使用 unknown 类型时可以使用 as 类型断言来明确类型

下面是 any 与 unknown 赋值上的区别,unknown 需要明确类型后赋值,any 则不需要

js
const xj: any = 'tydumpling'
const fn: unknown = 'tydumpling'

const a: string = xj
const b: string = fn // 报错: 'unknown'未知类型不能赋值给'string'类型

// unknown 类型需要明确类型后赋值
const c: string = fn as string

可以把任何值赋值给 unknown 类型,但在使用时需要指明类型

js
let fn: unknown
fn = 'tydumpling'
fn = 100

// 在使用时,TS不知道是什么类型,所以需要使用类型断言进行告之
const c = fn as number + 20

使用 keyof 类型工具时 unknown 与 any 的区别

js
type fn<T> = { [P in keyof T]: string }

// {[x: string]: string;}
type tydumpling = fn<any>

// 结果为{},因为 keyof unknow 是never,所以被忽略了
type XJ = fn<unknown>

不同类型赋值时会报错

js
const fn: string = '99'
const xj: number = fn as number // 报错,TS 认为字符串转数值会出现错误

这里需要使用 unknown 做个中间层转换,将其先转换为 unknown 未知类型,再转换为 string 类型

js
const fn: string = '99'
const xj: number = fn as unknown as number

any 与 unknown 在类型检查上是有区别的

js
let tydumpling: any
tydumpling.show()// any不进行类型检查,所以不会报错

let fn: unknown

fn.show()// unknown进行类型检查,unknown是未知类型所以报错

使用 any 类型 ts 不进行类型校验,所以在编译时不会报错,但执行编译后的 js 后会显示 NaN

js
function get(val: any) {
  val = val * 100
  return val
}

console.log(get('tydumpling')) // NaN

使用 unknown 类型时,结合 typeof 进行类型判断,根据不同类型使用不同逻辑

js
function get(val: unknown) {
  if (typeof val === 'number')
    return val * 100

  return 0
}

console.log(get(100)) // NaN

null & undefined

null 与 undefined 也是变量类型,用于定义值为 null 或 undefined

js
const fn: null = null
const tydumpling: undefined = undefined

console.log(fn, tydumpling)

下面是函数返回值的使用

js
function getName(): string | null {
  return null
}

console.log(getName())

strictNullChecks

当配置项启用 strictNullChecks 时,null 与 undefined 只能赋值给 void、null、undefined 类型

js
const fn: string = undefined // 配置strictNullChecks=true 时将报错

在TS中null与undefined使用与js是有区别的,下面的代码是有问题的,因为null没有toLowerCase()方法。但默认是不报错的,在tsconfig.json配置文件中定义 "strictNullChecks":true"strict": true 将会对代码进行报错提示。

js
function render(content: string) {
  console.log(content.toLowerCase())
}

render(null)

void

void 类型的值为 null 或 undefined,常用于对函数返回值类型定义

  • 严格模式(tsconfig.json 配置中关闭strict)时,void 值只能是 undefined(有关 TS 配置会在后面章节介绍)
  • 如果函数没有返回值请使用 void 类型,这会使用代码更易读,并可对不小心造成的函数返回内容进行校验
  • 你也可以将 void 理解为对返回 null 或 undefined 的函数返回值声明
  • TypeScript 中,不返回任何内容的 void 函数实际上返回的是 undefined

void 类型的值可以是 null 或 undefined,但如果 TS 配置开启了 strictstrictNullChecks则不允许 void 为 null

js
const fn: void = undefined
const tydumpling: void = null

void 不允许设置其他类型

js
let fn: void
fn = 'tydumpling.com' // 设置string 将报错

TypeScript 中,不返回任何内容的 void 函数实际上返回的是 undefined

js
function fn(): void {
}

let xj = fn()
xj = undefined

经过 void 限定后是不允许函数返回内容的,所以以下代码将报错

js
function fn(): void {
  return 'fn'
}

never

never 是任何类型的子类型,可以赋值给任何类型,没有类型是 never 的子类型。

never 类型的特点

  • never 没有任何子类型,所以任何类型都不可以赋值给 never
  • 函数抛出异常或无限循环时返回值是 never
  • 可以将每个类型理解为某类值的集合,比如 number 类型包含所有数字,但 never 类型没有任何值。
js
function fn(): never {
  throw new Error('出错了')
}

never 是所有类型的子类型,可以分配给任何类型,所以下面类型为 string

js
type tydumpling = never extends string ? string : boolean // string

其他类型不可以分配给 never 类型

js
type tydumpling = string extends never ? string : boolean // boolean

never 是所有类型的子类型,所以下面实际类型是 string | number

js
type tydumpling = never | string | number // string | number

总结

never表示永远不会到达,抛出异常就表示永远没有拿到返回结果,而void是有返回结果的,结果就是undefined

union 联合类型

union 联合类型是多个类型的组合,使用 | 进行连接,| 类似于 javascript 中的 || 或运算符。

下面是为变量声明字符串或数值类型

js
let fn: string | number = 'tydumpling.com'
fn = 2010

下面是为数组声明多种类型

js
const fn: (string | number | boolean)[] = []
fn.push('tydumpling.com', 2010, true)

也可以使用泛型方式声明(泛型的详细使用后面内容会介绍)

js
const fn: Array<string | number | boolean> = []
fn.push('tydumpling.com', 2010, true)

函数参数是联合类型时,可以使用 typeof 进行判断后分别处理,ts 会根据条件进行类型推断

js
interface fn {
  name: 'tydumpling'
}

function get(a: string | fn) {
  if (typeof a === 'string')
    a.includes('tydumpling')
  else
    return a.name

}

交叉类型

交差类型是将 interface、object 等进行合并,组合出新的类型

  • interface、object 进行属性合并
  • 交叉时要保证类型是一致的,string 与 number 交叉将得到 never 类型

对象类型会进行属性合并

js
interface A { name: string }
interface B { age: number }

const c: A & B = { name: 'tydumpling', age: 100 }

两个类型有相同属性,且类型不同时,返回类型为 never

js
const a = { name: 'tydumpling' }
const b = { age: 10, name: true }

type fn = typeof a & typeof b

// 报错 不能将类型“string”分配给类型“never”。
const c: fn = { age: 30, name: 'tydumpling' }

上面的问题可以使用 Pick 类型工具移除 name 索引

js
const a = { name: 'tydumpling' }
const b = { age: 10, name: true }

// 通过Pick移除name索引
type fn = typeof a & Pick<typeof b, 'age'>

const c: fn = { age: 30, name: 'tydumpling' }

通过交叉类型将 User 类型组合成新的 Member 类型

js
interface User { name: string; age: number }
type Member = { avatar: string } & User

const member: Member = {
  name: 'tydumpling', avatar: 'xj.png', age: 30
}

下面是属性合并函数的类型定义

js
function merge<T, U>(a: T & U, b: U): T & U {
  for (const key in b) {
    // a[key as Extract<keyof U, string>] = b[key] as any
    a[key] = b[key] as any
  }

  return a
}

string 和 number 因为类型不同,交叉计算后得到 never 类型

js
type fn = string & number

type fn2 = 'a' & 'b'

联合类型交叉

js
type fn = ('a' | 'b') & ('a') // a  因为字符串'b'与右侧联合类型交叉后得到never,所以被过滤了

type fn2 = ('a' | 'b') & ('a' | string) // a |b

函数

下面我们来掌握函数在 TypeScript 中的使用方式。

函数定义

下面是 TS 自动推断的函数类型

js
let fn = () => 'tydumpling'

fn = 'tydumpling.com' // 更改类型为字符串后将报错

我们可以使用 unknown 转为字符串,但这也没有意义

js
const a: string = fn as unknown as string

下面是使用显示类型定义函数 ,注意类型要使用大写的Function 这与 string/number/boolean 是有区别

js
let fn: Function
fn = () => 'tydumpling.com'
console.log(fn())

参数类型

下面是没有限定类型的函数定义,代码是不稳定的

js
function sum(a, b) {
  return a + b
}

console.log(sum('a', 3))// a3

因为这是个计算函数,下面来设置参数类型,让代码更健壮。

  • 因为限定了数值类型,所以函数参数必须传递数值
js
function sum(a: number, b: number) {
  return a + b
}

console.log(sum(2, 3))

如果参数是可选的,使用 ? 修饰

  • 下面的ratio 参数可以不传
  • 不传时ratio 值为undefined
js
function sum(a: number, b: number, ratio?: number) {
  return a + b
}

console.log(sum(3, 3)) // 6

如果参数设置默认值了就不需要可选参数符号?

js
function sum(a: number, b: number, ratio: number = 0.8) {
  return (a + b) * ratio
}

console.log(sum(3, 3)) // 4.8

返回值类型

下面是系统自动推断的参数返回值为 number

js
function sum(a: number, b: number) {
  return a + b
}
// 函数结构为 function sum(a: number, b: number): number

我们也可以明确返回类型

js
function sum(a: number, b: number): string {
  return `计算结果是:${a + b}`
}

console.log(sum(3, 3)) // 计算结果是:6

下面是箭头函数的表示方法

  • 因为函数体只有一条语句,所以省略了{}
js
const sum = (a: number, b: number): string => `计算结果是:${a + b}`

当函数没有明确返回值时,使用 void 类型。TS 会自动推断,建议明确声明 void 类型

js
function fn(): void {
  console.log('tydumpling')
}
fn()

参数声明

有时多个函数会用到相同的类型的参数,比如下面的示例

js
function addUser(user: { name: string; age: number }): void {
  console.log('添加用户')
}

function updateUser(user: { name: string; age: number }): void {
  console.log('更新用户')
}

updateUser({ name: 'tydumpling', age: 18 })

我们可以使用 type 对参数对象进行声明,通过这种复用的手段可以很好的优化代码

js
interface userType { name: string; age: number }

function addUser(user: userType): void {
  console.log('添加用户')
}

function updateUser(user: userType): void {
  console.log('更新用户')
}

updateUser({ name: 'tydumpling', age: 18 })

函数定义

对没有返回值函数的定义

js
let fn: () => void

fn = (): void => console.log('tydumpling')

下例是对 fn 函数的定义

  • 函数定义中声明的变量 a,在具体实现函数是可以为任何名称
js
let fn: (a: number, b: number) => number

fn = (x: number, y: number): number => {
  return x + y
}

也可以在声明函数时就定义函数的结构

js
const fn: (a: number, b: number) => number = (x: number, y: number): number => {
  return x + y
}

console.log(fn(2, 3))

参数是对象结构的函数定义

  • 下例中的参数 u 不定义类型结构,TS 也是可以推断出来的
js
let addUser: (user: { name: string; age: number }) => boolean

addUser = (u: { name: string; age: number }): boolean => {
  console.log('添加用户')

  return true
}

上例中使用了重复的参数描述 { name: string, age: number } ,下面我们将参数对象使用 type 进行描述,就可以很好的优化代码

js
interface userType { name: string; age: number }

let addUser: (user: userType) => boolean

addUser = (u: userType): boolean => {
  console.log('添加用户')

  return true
}

addUser({ name: 'tydumpling', age: 12 })

上面是将参数使用 type 进行了描述,我们也可以将函数结构使用 type 进行描述

js
interface userType { name: string; age: number }

type addUserFunc = (user: userType) => boolean

const addUser: addUserFunc = (u: userType): boolean => {
  console.log('添加用户')

  return true
}

addUser({ name: 'tydumpling', age: 12 })

剩余参数

下面的求合函数接收多个参数

js
function sum(...args: number[]): number {
  return args.reduce((s, n) => s + n, 0)
}

console.log(sum(1, 2, 3, 4, 5)) // 成功,参数要的是数值数组
console.log(sum(1, 2, 3, 4, '5')) // 失败,不能有字符串

下面通过第二个参数接收剩余参数,来实现数据追加的示例

js
function push(arr: any[], ...args: any[]): any[] {
  arr.push(...args)
  return arr
}

const fn: any[] = ['tydumpling.com']

console.log(push(fn, 'TS', 'tydumpling')) // [ 'tydumpling.com', 'TS', 'tydumpling' ]

Tuple 元组

元组与数组类似,但元组要为每个值进行类型声明。

数组只是定义了值的类型,并没有约束某个位置的值必须是什么类型,请看下例

js
const arr: (number | string | boolean)[] = ['tydumpling', 2030, true]

arr[1] = 'tydumpling.com' // 不会报错,可以将原来是数值的更改为字符串,这是数组允许的类型范围
arr[10] = 'TS老师' 			// 不会报错,类型也是允许的
console.log(arr)

下面看使用元组来限制值的类型

js
const fn: [string, number] = ['tydumpling', 2030]
fn[0] = true // 报错,第一个值必须是字符串

函数重载

函数的参数类型或数量不同时,会有不同的返回值,函数重载就是定义这种不同情况的函数。

重载签名

重载签名是对函数多种调用方式的定义,定义不同的函数参数签名与返回值签名,但是没有函数体的实现。

  • 使用函数时调用的是重载签名函数,在 vscode 代码跟踪时也会定位到重载签名
  • 将从第一个重载签名尝试调用,向下查找是否有匹配的重载签名
  • 定义重载签名可以在 idea、vscode 中拥有更好的代码提示
js
function getId(id: string): string
function getId(id: number): number

实现签名

实现签名是是函数功能的实现,对参数与返回值要包扩符合函数签名的宽泛类型。

  • 重载签名可以是多个,实现签名只能是一个
  • 实现签名是最终执行的函数
  • 用户在调用时调用的是重载签名
  • 重载签名可被调用,实现签名不能被调用
  • 实现签名要使用通用类型
js
// 重载签名
function getId(id: string): string
function getId(id: number): number

// 实现签名
function getId(id: unknown): unknown {
  if (typeof id === 'string')
    return id

  return id
}

// function getId(id: string): string (+1 overload)
getId('tydumpling')

实现签名要使用通用的类型

js
function getId(id: string): string
function getId(id: number): number

// 报错:因为实现签名不通用 「不能将类型“unknown”分配给类型“string”」
function getId(id: unknown): string {
  if (typeof id === 'string')
    return id

  return id
}

getId('tydumpling')

Contributors

Yuan Tang