类型定义
同 js 一样, ts 中函数分为两种形式,只不过我们可以给参数和函数的返回值定义上类型:
- 函数声明(命名函数)
typescript
function fn(day: string): string {
return '今天是' + day
}
console.log(fn('周天')) // 今天是周天
如此,传入函数的参数的类型和个数就被限制了。
- 函数表达式(匿名函数)
typescript
const fn = function(day: string): string {
return '今天是' + day
}
console.log(fn('周天')) // 今天是周天
注意:
- 函数的返回值,其实也可以不写,因为 ts 会根据
return
的值进行类型推导,当然明确写出来可以增加代码的可读性; - 对于某些回调函数的定义,参数如果可以根据上下文推导出类型(contextual typing),则可以不明确写出。比如下面的
item
, 根据list
可以知道必为string
, 就可以不写类型:
typescript
const list = ['Jay', 'Eson']
list.forEach(item => console.log(item))
函数的完整写法
typescript
const fn: (day: string) => string = function(day: string): string {
return '今天是' + day
}
console.log(fn('周五')) // 今天是周五
(day: string) => string
被称为函数类型表达式(function type expression),是函数 fn
的类型,或许你会觉得这里的 day
反正是个形参,可以省略不写,但 ts 是要求必须写的。如果直接写 (string) => string
,ts 会认为这个函数是接收一个类型为 any
的参数,参数的名字叫 string
。
调用签名(Call Signature)
通过接口的方式作为函数的类型来使用,可以描述带有属性的函数。
typescript
// 定义函数接口
interface IAddFn {
name: string,
(a: number, b: number): number
}
// 定义函数
const myAddFn: IAddFn = function (a, b) {
return a + b
}
console.log(myAddFn(1, 2)) // 3
console.log(myAddFn.name) // myAddFn
(a: number, b: number): number
就是调用签名, 定义了参数和返回值的类型。注意调用签名是使用:
而不是=>
:
typescript
interface IPerson {
run: () => void
}
run: () => void
表示的是 IPerson
类型有个属性 run
为函数,而调用签名则表示该类型的变量是可以调用的。
- 使用函数表达式 来定义类型是上述接口的函数,函数的参数和返回值的类型可以省略不写,ts 的类型系统会根据
IAddFn
接口推断出参数类型。
一般对于函数的类型,还是使用 type
定义阅读性更好:
typescript
type AddFnType = (a: number, b: number) => number
构造签名(Construct Signature)
如果想表示一个函数是构造函数(类),必须要通过 new 调用,则可以在函数的调用签名前加上 new
关键字:
typescript
interface IAdd {
new(a: number, b: number): number
}
const fn1 = function (foo: IAdd) {
new foo(1, 2) // 如果直接调用 foo(1, 2) 则会报错
}
// 也可以直接在函数类型表达式前加上 new ,表示 foo 需要为构造函数
const fn2 = function (foo: new () => void) {
new foo()
}
默认参数和可选参数
默认参数与 ES6 中的 js 里写法一样,在参数后用 =
号定义默认值,可选参数则是在参数后加个 ?
默认参数
当参数有默认值时,可以不显示地定义参数的类型,而依靠类型推导:
typescript
// 默认参数
function fn(day = '周五'): string {
return '今天是' + day
}
const result = fn()
console.log(result) // 今天是周五
有多个参数时,默认参数不像可选参数那样一定要放在最后,在下例中就是放在了第一位,但是在调用参数时,必须传递第二个参数,只不过第一个传 undefined
代表使用默认值:
typescript
const add = (num1 = 1, num2: number) => {
console.log(num1 + num2)
}
add(undefined, 2) // 3
所以一般有多个参数时,其顺序最好是:普通参数 -> 有默认值的参数 -> 可选参数。
可选参数
typescript
// 可选参数
function fn(day?: string): string {
return '今天是' + day
}
const result = fn()
console.log(result) // 今天是undefined
可选参数的类型其实是和 undefined
的联合类型。在 js 中,所有参数都是可选参数,即不赋值时, 它的值就是 undefined
。
注意,如果有多个参数,那么可选参数需要放最后一个:
typescript
const add = (num1: number, num2?: number) => {
return num2 ? num1 + num2 : num1
}
如果使用了可选参数,有时会遇到问题,比如下面这样写就会报错,因为 day 可能为 undefined
:
解决的办法不止一种:
非空类型断言
如果你十分确定在调用 fn()
时都传了参数,也就是 day
一定有值,那么比较快捷的办法是使用非空类型断言 !
,让 ts 在编译阶段跳过对它的检测:
typescript
function fn(day?: string) {
console.log(day!.length)
}
可选链
如果不确定 day
到底存不存在,也可以使用 js 就有的语法 ------ 可选链 ?.
:
typescript
function fn(day?: string) {
console.log(day?.length)
}
如果是赋值操作,就不能用可选链了,但是依旧可以使用非空断言,或者是类型缩小:
typescript
function fn(day?: string) {
day!.length = 3
if (day) {
day.length = 3
}
}
剩余参数
与 ES6 中的剩余参数基本一致,只是可以添加个类型
typescript
function eat(food1: string, ...foods: string[]) {
console.log(food1) // 麦辣鸡腿堡
console.log(foods) // ["麦辣鸡翅", "薯条", "可乐"]
}
eat('麦辣鸡腿堡', '麦辣鸡翅', '薯条', '可乐')
函数作为函数的参数
当参数个数少于定义的时候不校验。
当一个函数 fn
作为参数传给另一个函数 foo
时,如果 fn
的参数个数少于 foo
定义时声明的 fn 的参数的个数,ts 是不会报错的。比如下例中 foo
的第 3 个参数 fn
应该有两个参数 a
和 b
,但是实际上在第 4 行传给 foo
的函数一个参数也没写,也不会报错:
typescript
function foo(m: number, n: number, fn: (a: number, b: number) => void) {
fn(m, n)
}
foo(1, 2, () => {
console.log('hello')
})
请注意:
- 在第 2 行执行
fn()
时,也就是fn
被调用时,还是必须得有 2 个参数的; - 如果在第 4 行传给
foo
的函数的参数的个数多于 2 个,则依旧会报错;或者m
和n
传的不是数字 1 和 2 而是字符串或别的类型,也会报错; - 虽然在第 4 行作为参数传给
foo
时可能没有a
、b
参数,但因为我们在第 2 行调用fn
时,其一定是有 2 个参数的,所以无需为了fn
作为foo
的参数传入时可能没有参数而在第 1 行定义类型时将a
、b
变为可选参数。官方文档说明 - ts 有这种规则的好处在于,像传给 forEach 的这个函数,照理也是有 3 个参数的 ------
value: number, index: number, array: number[]
------ 但我们可以像下面这样只传一个:
typescript
const arr = [1, 2, 3]
arr.forEach(item => console.log(item))
函数重载
函数名相同, 而形参不同(可以是个数不同,也可以是类型不同)的多个函数。也就是可以声明同一个函数的几种不同的情况,比如参数不同,返回值不同,方便错误检查。
比如我们需要个 add
函数,只希望存在两种情况:
- 两个参数都是数字,返回相加的结果
- 两个参数都是字符串,返回拼接的结果
typescript
function add(x: number | string, y: number | string): number | string {
if (typeof x === 'number' && typeof y === 'number') {
return x + y // number 类型的
} else if(typeof x === 'string' && typeof y === 'string'){
return x + y // string 类型的
} else {
return '错误'
}
}
console.log(add(1, 2)) // 3
console.log(add('1', '2')) // 12
console.log(add(1, '2')) // 错误
上述使用联合类型的写法,当传入的参数一个为数字一个为字符串时,vs code 并不会有标红的波浪线提示错误。而用函数重载可以解决这个问题,也就是在函数声明前进行重载签名(overload signature):
typescript
function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a, b) {
return a + b
}
add(1, 2)
add('hellow', 'world')
现在,当传入 add
的参数均为 number 时,add
的类型如下:
当传入的都是 string 时:
注意:
- 第 1~ 2 行的重载签名是没有执行体的,具体的执行是写在第 3 ~ 5 行的实现函数,在实现函数中我们对
a
和b
的类型就可以写得宽泛些,比如不写就是默认 any,当然也可以是联合类型 "string | number"。 - 实现函数是不能被直接调用的,当我们调用
add
时,ts 会从上到下的去匹配重载签名,如果匹配不到就会报错,所以即使第 3 行这看起来a
和b
可以是任意类型,但如果传给add
的是一个数字和一个字符串,就会有标红提示了:
条件类型
还可以使用条件类型(Conditional types)将上例中的 2 行重载前面改为只需 1 行:
typescript
function add<T extends number | string>(a: T, b: T): T extends number ? number : string
function add(a: any, b: any) {
return a + b
}
条件类型的写法形同三元运算符,通过判断 T
是否 extends number
来决定函数的返回值到底是 number
还是 string
。
P.S. 如果不写成函数重载的形式而是直接在函数上应用条件类型来判断,会有如下报错:
this
像下面这样写:
typescript
const singer = {
name: 'Jay',
sing() {
console.log(`是谁在唱歌? 是${this.name}`)
}
}
singer.sing()
在第 7 行调用 sing
方法时,在有使用 tsc --init
生成默认配置的 tsconfig.json 的情况下,ts 是会根据上下文推导出第 4 行的 this
指向的是 singer
的:
但是如果改成下面这样,单独声明一个函数 sing
,然后赋值给 singer
的 sing
属性:
typescript
function sing() {
console.log(`是谁在唱歌? 是${this.name}`)
}
const singer = {
name: 'Jay',
sing
}
singer.sing()
此时严格模式下(tsconfig.json 配置了 "strict": true
),ts 无法判断 this
的指向,编译就会报错:
解决的办法有 2 个:
- 直接在 tsconfig.json 中配置:
"noImplicitThis": false
,即允许this
隐式具有any
类型; - 往
sing
内传入this
作为第 1 个 参数(如果有其它参数就放在 this 后面就好),此处this
的类型为一个有name
属性的对象:
typescript
function sing(this: { name: string }) {
console.log(`是谁在唱歌? 是${this.name}`)
}
现在执行 singer.sing()
就不会报错了,但是现在如果直接执行 sing()
会报错:
可以通过 call
将 this
指定为一个含有 name
属性的对象来调用:sing.call({ name: 'Jay' })
。