第五节:TypeScript函数

TypeScript 函数

函数是任何应用程序的基本构建块,无论它们是本地函数、从另一个模块导入的函数,还是类中的方法。它们也是值,就像其他值一样,TypeScript 有很多方法来描述如何调用函数。

了解编写函数类型之前, 先了解一下如何为函数参数以及函数返回值添加类型注释。


1. 函数的参数类型注释

函数是在JavaScript中传递数据的主要方式.TypeScript 允许您指定函数的输入和输出值的类型。

声明函数时,可以在每个参数后面添加类型注释, 声明函数接受那些类型的参数.参数类型注释在参数名称:之后

例如:

// 带有参数类型的函数
function greet(name:string){
    console.log(`hello ${name.toUpperCase()}`)
    // (parameter) name: string
}

当参数具有类型注释时, 将检查该函数参数的类型, 参数类型不匹配将报错

例如

greet(20)
// 报错: 类型“number”的参数不能赋给类型“string”的参数。


2. 返回类型注释

除了可以给函数参数添加类型注释, 还可以给函数的返回值添加类型注释.

返回的类型注释出现在参数列表之后

function greet():number {
  return 30
}

与变量类型注释非常相似, 函数通常不需要返回类型注释, 因为TypeScript会更加函数的返回值推断出函数的类型.

而一些代码库中明确指定返回类型以用于文档为目的, 防止意外更改, 在这可能就处于个人喜好.


那么接下来就让我们了解如何编写函数的类型


3. 函数类型表达式

描述函数最简单的方式就是使用函数类型表达式. 类型在语法上有点类似于箭头函数

例如:

// 变量添加函数类型的类型注释
let myFn:(name:string) => number;
// let myFn: (name: string) => number
/*
  函数类型注释表面myFn变量将是一个函数类型
  而且还是一个接受string类型参数, 并返回number类型的函数
*/

// ok
myFn = (age:string) => {
  return Math.random()
}
// 虽然参数命名不一样, 但是参数类型符合类型注释

// 参数类型不符
myFn = (name:number) =>{
  return  26
}
/*
  报错
    不能将类型“(name: number) => number”分配给类型“(name: string) => number”。
    参数“name”和“name” 的类型不兼容。
    不能将类型“string”分配给类型“number”。
*/


// 返回值类型不符
myFn = (age:string) => {
  return 'hello'
}
/*
  报错:
    不能将类型“(age: string) => string”分配给类型“(name: string) => number”。
    不能将类型“string”分配给类型“number”。
*/


当然也可以声明变量时直接初始赋值,只不过会比较长

let myFn:(name:string) => number = (s:string) => {
    return 30
}

要注意区分,哪里到哪里是函数类型, 哪里到哪里是真实的函数值


也可以给函数参数添加函数类型的类型注释

// 函数参数添加函数类型的类型注释
function greeter(fn: (x:string) => void){
  fn('hello world')
}


// 执行函数
// 作为参数使用的函数比喻符合参数类型
function print(s:string){
  console.log(s)
}

// 执行函数
greeter(print)

示例中,参数fn的类型是一个(x:string)=> void, 表示fn类型是带有一个string类型参数,没有返回值的函数.

就像函数声明一样, 如果函数参数没有指定类型, 则参数是隐式的any类型

function example(s){
  console.log(s)
  // (parameter) s: any
}


还有一点需要注意的是, 函数类型 参数必须有参数名

例如:如下函数类型不加参数名的情况

function greeter(fn: (string) => void){
  // (parameter) string: any
  fn('hello world')
}

说明:

  1. (s:string) =void 函数类型表示接受一个string类型参数, 无返回值
  2. (string) => void函数类型中, string被解析为类型名称,而不是类型, 表示函数接受一个默认any类型的参数,无返回值


4. 函数类型推断

跟变量类型注释一样, 有的是有我们并不需要明确的指定类型, 因为TypeScript会更加我们赋予的值推断出类型

函数类型也是一样.

例如:

// 1.变量没有添加类型注释, TypeScript根据赋值函数类型推断
let myAdd = function (x:number, y: number):number {
  return x + y
}
// let myAdd: (x: number, y: number) => number

// 2. 变量添加函数类型注释
let myAdd2: (value1: number, value2: number) => number = function (x, y){
  return x + y
}
// let myAdd2: (value1: number, value2: number) => number

示例中: 两个函数表达式的方式定义的函数, 一个添加了类注释,一个没有添加类型注释,但是变量的类型是完全相同的.


5. 函数类型别名

5.1 使用类型别名定义函数类型

当然,直接通过使用函数类型来进行类型注释,复用性不高, 因此可以将函数类型抽离使用类型别名,或通话签名

// 类型别名
type GreetFunction  = (s:string) => void

// 函数参数添加类型注释
function greeter(fn:GreetFunction){
  fn('hello world')
}


5.2 调用签名

在JavaScript中, 函数除了可调用之外还可以具有属性. 但是,函数表达式语法不允许声明函数属性.

如果像使用属性描述可调用的函数, 我们可以在对象类型中编写调用签名

例如:

//  类型别名(通过调用签名声明函数类型)
type GreetFunction  = {
  (s:string): void
}

// 函数参数类型为函数类型
function greeter(fn:GreetFunction){
  fn('hello world')
}

请注意,调用签名与函数表达式相比,语法略有不同, 参数列表和返回返回类型之间的=>修改为对象类型属性和值类型中中键的:


除了调用签名,还可以声明函数上其他的属性类型

//  类型别名(通过调用签名声明函数类型)
type GreetFunction  = {
  descript: string  // 函数属性
  (s:string): void
}

// 函数参数类型为函数类型
function greeter(fn:GreetFunction){
  console.log(`${fn('hello')} ${fn.descript}`)
}

// 作为参数的函数
function example(x:string){
  return x
}

// 调用
greeter(example)
/*
  报错:
    类型“(x: string) => string”的参数不能赋给类型“GreetFunction”的参数。
    类型 "(x: string) => string" 中缺少属性 "descript",但类型 "GreetFunction" 中需要该属性。
*/ 

示例报错的原因在于参数类型中的函数有descript属性, 但是我们传入的函数没有此属性


给参数函数添加属性,重新调用

// 作为参数的函数
function example(x:string){
  return x
}
example.descript = 'world'

// 调用
greeter(example)

此时代码就没有任何报错了


除了可以使用类型别名来定义带有调用签名的函数类型, 接口也可以实现相同的功能

例如:

//  接口声明带有调用签名的对象类型
interface GreetFunction   {
  descript: string  
  (s:string): void
}

// 函数参数类型为函数类型
function greeter(fn:GreetFunction){
  console.log(`${fn('hello')} ${fn.descript}`)
}

其实所谓的调用签名本事上就是一个对象类型, 其中有一个属性为调用签名,表示可以像函数一样调用, 除此之外还可以定义其他属性


需要注意的时, 函数参数的名称不必与调用签名中参数名一致, 只要对应的位置类型匹配即可

// 定义检测函数类型的接口
interface MyFunction{
  // 调用签名:形参第一个为string类型,第二个为number类型, 函数返回boolean类型
  (params: string, subString:number):boolean  
}

// 类型注释
let func: MyFunction;

// 形参第一个参数为string类型, 第二个为number类型
func = function(p:string, s:number){
  return s > 1   // 返回布尔类型
}


func('张三',10)

其实函数形参的类型已经在接口中定义,因此函数形参可以不定义类型,会自动使用接口中的类型来验证实参数据

// 定义检测函数类型的接口
interface MyFunction{
  // 调用签名:形参第一个为string类型,第二个为number类型, 函数返回boolean类型
  (params: string, subString:number):boolean  
}

// 类型注释
let func: MyFunction;

//  形参不用添加类型, 会自动用接口中调用签名参数的类型来匹配
func = function(p, s){
  return s > 1   // 返回布尔类型
}


func('张三',10)


5.3 构造签名

JavaScript函数也可以通过new操作符来调用, 也就是我们常说的构造函数.用来创建新对象的函数.

在TypeScript中,也可以通过在调用签名前添加new关键字来编写构造函数签名

例如:

// 接口定义对象类型
interface Person{
  name:string;
  age: number;
}


//  类型别名(构造函数签名)
type GreetFunction  = {
  new (n:string): Person // 构造函数签名(返回对象类型)
}

// 函数参数类型为构造函数
function greeter(fn:GreetFunction){
  // 参数是一个构造函数
  return new fn('张三')
}

//  类
class Student{
  name:string
  age: number
  constructor(n:string){
    this.name = n
    this.age = 18
  }
}

// 调用
const student = greeter(Student)
// const student: Person
console.log('student', student)
// {age: 18, name: "张三"}


6. 可选参数与默认参数

6.1 实参必须与形参个数保持一致

TypeScriptl里的每个函数参数都是必须的传递的,TypeScript会检查用户是否为每个参数都传入了值

// 声明函数
function getUser(name:string, age:number) {
    return {ame,age}
}

let user = getUser()
// 报错: 应有 2 个参数,但获得 0 个

let user2 = getUser('小明') 
//  报错: 应有 2 个参数,但获得 1 个

let user3 = getUser('小明',18, '男')  
// 报错: 应有 2 个参数,但获得 3 个

let user4 = getUser('小明',18)    // 正常

示例中, 函数调用时传递给函数的实参个数必须与函数期望值(形参)的参数个数保持一致

但在JavaScript中函数的参数通常采用的都是可变数量的参数, 也就是说函数声明时定义三个形参,但在函数调用时, 传递的参数数量并不受限制.


6.2. 可选参数

而在TypeScript里我们可以通过?将参数标记为可选参数.

例如:

// age标记为可选参数
function getUser(name: string, age?: number) {
    return {ame,age}
}

// 因为age是可选参数, 因此传值不传值都不会报错
let user1 = getUser('小明')      // 正常  
let user2 = getUser('小明',18)   // 正常 

let user3 = getUser('小明',18, '男')
// 报错: 应有 1-2 个参数,但获得 3 个

说明

  1. 可选参数必须跟在必须其他普通参数的后面, 因为函数的参数是按顺序传递的
  2. 可选参数会影响函数参数的个数(如,示例中函数应该有1个参数或2个参数)


如果将可选参数放在普通参数之前就会报错

function getUser(name?: string, age: number) {
    return {ame,age}
}
// 报错:必选参数不能位于可选参数后


6.3 默认参数

在TypeScript里,我们也可以为参数提供一个默认值. 当用户没有传递实参或传递的实参值是undefined时。 启用默认值.

// 参数带有默认值的函数
function getUser(name: string, age=18) {
    return {ame,age}
}

 // 启用默认值
let user1 = getUser('小明') 

 // 使用实参值
let user2 = getUser('小明',16) 

// 显示的传递undefined, 也会启用默认值, 跟JavaScript逻辑一样
let user3 = getUser('小明',undefined)  

注意: 带默认参数的形参放在所有形参后面, 如果放在前面需要启用默认值,必须手动的传递undefined


一般带有默认值的参数不用添加类型注释, 因为TypeScript会根据默认值推断类型.

function getUser(name: string, age=18) {
    return {ame,age}
}

getUser('小明', 'hello') 
// 报错: 类型“string”的参数不能赋给类型“number”的参数


7. 函数重载

在TypeScript中,我们可以通过编写重载签名来指定一个可以以不同方式调用的函数.

通常都是先编写同名函数签名,然后在是函数体

例如: 编写一个函数生成时间, 参数可以接收一个时间戳参数, 也可以接收年月日三个参数

// 函数重载签名
// 只有一个参数(时间戳)的函数签名
function makeDate(timestamp:number):Date
// 有三个参数(年月日)的函数签名
function makeDate(y:number, m:number, d:number):Date
// 函数签名必须有实现的函数体
// 要满足两个函数签名, 函数体,第一个但是必须存在,有可能是两个函数签名中任意一个
// 而第二第三个参数在满足第一个函数签名时可能不存在, 定义为可选参数
function makeDate(yOrTimestamp:number, m?:number,d?:number ){
    // 判断第二第三个参数是否存在
    if(m !== undefined && d !== undefined){
        return new Date(yOrTimestamp,m,d)
    }else{
        return new Date(yOrTimestamp)
    }
}

// 使用重载函数
// ok 参数可以是一个或三个
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);

// 报错
const d3 = makeDate(1, 3);
// 报错: 没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载。

在这个例子中,我们写了两个重载:一个接受一个参数,另一个接受三个参数。前两个签名称为重载签名

然后编写了一个具体兼容签名的函数实现.


8. reset参数

8.1 剩余参数

TypeScript中,除了可以使用可选参数或重载来定义可以 接受各种固定参数计数的函数外,我们还可以使用剩余参数定义接受无限数量的函数

reset参数出现在所有其他参数之后, 并使用...语法

function getUser(name: string, age:number, ...arg: string[]) {
    // ...剩余运算符将所有形参没接受完的实参组成数组
    return {name,age,hobby: arg}
}

let user = getUser('小明',18, '游泳','游戏','篮球')

console.log('user', user)

剩余参数会将所有没有变量接受的实参放入数组中


在Type中, 剩余参数的类型注释是隐式的any[]类型 而不是any,普通参数的隐式类型为any.

在这给剩余参数添加类型注释时必须是Array,T[]或元组类型.否则会报错

function getUser(name: string,  ...arg: string) {
    // 报错: rest 参数必须是数组类型。
}


8.2 扩展运算符

相反, 我们也可以使用扩展语法从数组中获取可变数据参数, 例如,数组的 push方法可以接受任意数量的参数

let arr = [1,2,3]
let arr2 = [4,5,6]

// 扩展arr2
arr.push(...arr2)

请注意,通常,TypeScript并不假定数组 是不可变的, 也就是数组内的数量是不确定, 因此就可能会导致令人惊讶的行为

例如:

const args = [8, 5];
const angle = Math.atan2(...args);
// 报错:扩展参数必须具有元组类型或传递给 rest 参数。

示例报错了, 此时将鼠标移入atan2方法上就会发现

(method) Math.atan2(y: number, x: number): number

atan2 接受的是两个数字类型的参数, 但是args的参数是不定的, 扩展之后并不能匹配,因此这就是TypeScript报错的原因

同时TypeScript报错告诉我们的是,函数或方法中如果像使用扩展语法, 前提这个方法必须具有剩余参数或传递的参数为元组类型(因为元组类型数量固定)

那我们可以看下push方法的类型是什么? 为什么push可以扩展, 鼠标移入push

(method) Array.push(...items: number[]): number

我们会发现push类型参数是剩余运算符, 也就是说不管你传递多少参数, 会被剩余参数打包到数组中

其实我们自己知道我们 给atan2方法传递的数组只有两项目,扩展后是完全符合参数调节, 但TypeScript识别数组是项,

因此解决这个问题的最佳方案就是使用使用断言为const, 将数组类型转为元组类型.

const args = [8, 5];
// const args: number[]
const args2 = [8, 5] as const; 
// const args2: readonly [8, 5]
const angle = Math.atan2(...args2);

原有的args 是一个number[]类型, 通过as const断言后,args变成了只读的元组类型


9. 参数解构

也可以使用参数解构 来方便地将作为参数提供的对象解压到函数体中的一个或多个局部变量中.

JavaScript中解构是这样的

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

sum({a:10, b:20, c: 30})


此时加上TypeScript中对象类型注释就变成这样

// 解构参数: 对象类型
function sum({a,b,c}: {a:number,b:number,c:number}){
    return a + b + c
}

sum({a:10, b:20, c: 30})


还可以将对象类型通过类型别名或接口抽离

// 类型别名
type ABC = {a:number,b:number,c:number}

// 解构参数: 对象类型
function sum({a,b,c}:ABC ){
    return a + b + c
}

sum({a:10, b:20, c: 30})


10. 通用函数

10.1 泛型函数

通常会编写一个函数, 其中输出的类型和输入的类型相关,或者两个输入的类型类型以某种方式相关.

例如:返回 数组第一个元素函数

function firstElement(arr:any[]){
    return arr[0]
}
// 类型: function firstElement(arr: any[]): any

这个函数,接受一个any[]类型的数组, 并返回any类型,我们希望的是如果能返回函数具体的类型会更好


在TypeScript中, 当我们想要描述两个值之间的对应关系, 会使用泛型, 通过在函数签名中声明一个类型参数来做到这一点

这个类型参数当成函数参数来理解, 只不过函数参数是用来接受值的, 而类型参数是用来接受类型的,

就像类型别名可以当成变量理解一样

例如:

// 变量num 接受了值
const num = 10

// 类型别名接受一个类型
type Hello = string

而类型参数也是如此

例如:

// 普通函数
function fn(num){
    // num 参数接受一个值, 
    // 在函数体内通过num来一直调用这个值
    console.log(num)
}


// 泛型
function firstElement(arr:Type[]):Type | undefined{
    // <>中的Type就是类型参数, Type接受一个类型
    // 函数体内, 包括参数类型, 返回类型都可以通过类型参数使用同一个类型
    return arr[0]
}

通过向这个函数添加一个类型参数Type并在两个地方使用它,我们在函数的输入(数组)和输出(返回值)之间创建了一个链接。现在当我们调用它时,会出现一个更具体的类型:


/*
    在调用函数的使用, 我们传入了类型string
    那么firsetElement 函数内Type类型参数就是一个string类型
    函数会接受一个string[]数组, 并返回 string | undefined 联合类型
*/ 
const s = firstElement(['hello', 'world'])
// const s: string

// 同样可以传入其他类型
const n = firstElement([10, 20])
// const n: number


注意, 你如果指定了Type是某种类型, 例如string类型, 那么函数firstElement参数就通过Type关联这, 需要接受一个string[], 如果你传入的参数不对将会报错

/*
   指定Type 是string类型, 传递的number类型就不匹配
*/ 
const s = firstElement(['hello', 'world', 20])
// const s: string
// 20报错: 不能将类型“number”分配给类型“string”


10.2 泛型的推理

有事我们不必明确指定Type的类型, 类型可以有TypeScript推断得到

例如:上例中的调用我们可以去掉指定类型

/*
    调用函数时没有在<>中明确指定Type类型
    但是我们将[10,20] 传递给函数时, 函数推断出为number[]类型
    而number[] 有时需要去匹配Type[] 类型的, Type  并没有指定
    因此TypeScript推断出  Type类型参数为 number类型
*/ 
const n = firstElement([10, 20])
// const n: number


也可以使用多个类型参数

例如:

/*
    函数map中有两个类型参数 Input Output
    函数普通参数接受两个参数
     1. arr为Input[]
     2. func是一个函数, 此函数接受一个Input类型(arr的项),返回Output类型
    函数整体返回 Output[]

*/
function map(arr:Input[], func: (arg:Input) => Output): Output[]{
    return arr.map(func)
}

/*
    调用函数时没有指定Input,Output类型
    通过第一个参数['1','2','3'] Input 是string 类型
    第二个参数是函数
        1.函数参数n的类型就是string类型
        2.函数返回值 parseInt(n)就是一个 number类型, 因此推断 Output是一个number类型
    函数map整体返回Output[]  也就是 number[]类型

*/
const result = map(['1','2','3'], (n) => parseInt(n))
// const result: number[]


10.3 约束

编写通用函数,处理任何类型的值, 但有时我们可能需要使用约束来限制类型参数可以接受类型的种类

例如:编写一个函数比较两个参数length属性的长度

function longest(a:Type,b:Type){
    if(a.length >= b.length){
        return a
    }else{
        return 
    }
}
// 报错:类型“Type”上不存在属性“length”。

如果这么编写通用函数, 那么就会报错, 告诉我们类型Type上不存在length属性

Type只是类型参数, 可以是任何类型,如果调用函数传入number类型, number是没有length属性的

因此我们可以通过约束来限制Type 只能接受大有length属性的参数


通过编写一个字句将类型参数限制为具有length属性对象类型, 然后通过extends来扩展Type类型参数

例如:

function longest(a:Type,b:Type){
    if(a.length >= b.length){
        return a
    }else{
        return 
    }
}
// 此时通用函数就不报错, 但是调用函数传入的参数必须满足具有length属性

// Type 为 'number[]'
const arr = longest([1, 2], [1, 2, 3]);

// Type 为 'alice' | 'bob'
const str = longest("alice", "bob");

// 报错
const notOK = longest(10, 100);
// 类型“number”的参数不能赋给类型“{ length: number; }”的参数。


10.4 指定类型参数

TypeScript 通常可以在泛型调用中推断出预期的类型参数,但并非总是如此

例如:

function combine(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

// 如果此时使用不匹配的数组调用函数就会报错
const arr = combine([1, 2, 3], ["hello"]);
//不能将类型“string”分配给类型“number

示例中, TypeScript通过第一个参数推断出Type的类型为number类型, 紧接着你就讲一个string[]赋值给一个类型为number[]的参数, 就报错了


如果你打算真的这样传递参数, 那么你可以手动的指定Type 为string | number联合类型

const arr = combine([1, 2, 3], ["hello"]);

你可能感兴趣的:(第五节:TypeScript函数)