【TypeScript】深入学习TypeScript函数

目录

  • 前言
    • 1、函数类型表达式
      • 对象内使用函数类型
    • 2、调用签名
    • 3、构造签名
    • 4、泛型函数(通用函数)
      • 类型推断
      • 指定类型参数
      • 限制条件
      • 编写规范
    • 5、可选参数
    • 6、函数重载
      • 重载签名与实现签名
      • 编写规范
      • 在函数中声明this
    • 7、参数展开运算符
      • 形参展开
      • 实参展开
    • 8、参数解构
    • 9、函数的可分配性
  • 结语

前言

最近博主一直在创作TypeScript的内容,所有的TypeScript文章都在我的TypeScript专栏里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅

今天呢,我们将深入去学习TypeScript中的函数

1、函数类型表达式

函数类型格式为: (param:Type) => returnType

  • Type代表参数的类型(如果没有指定参数类型,它就隐含为 any 类型),returnType为函数返回值的类型
  • 支持多个参数和可选参数: (a:number,b:string) =>void
  • returnTypevoid时,代表函数没有返回值

声明一个函数类型FnType

// 类型别名方式
type FnType = (params: number) => void;

// 接口方式
// interface FnType {
//     (params: number): void;
// }

正确使用FnType

const fn1: FnType = (a: number) => {}; 
fn1(1);

这里定义fn1函数时可以不手动定义形参的类型,因为TypeScript会根据其使用的函数类型(FnType)自动推断出形参的类型:

const fn1: FnType = (a) => {}; // ok: a自动推断出为number类型,效果同上

错误使用FnType

 // err: 不能将类型“(a: any, b: any) => void”分配给类型“FnType”
const fn3: FnType = (a, b) => {}; // 形参数量不对
// err: 参数“a”和“params” 的类型不兼容,不能将类型“number”分配给类型“string”。
const fn4: FnType = (a: string) => {}; // 形参类型与FnType类型中不合
  • 有一点需要注意,当使用函数类型FnType的函数不具有形参时,TypeScript并不会报错:

    const fn2: FnType = () => {}; // ok: 声明函数时不带参数不会报错
    

    但是调用fn2时依旧需要传入函数类型FnType中定义的参数数量:

    fn2(); // err:应有 1 个参数,但获得 0 个
    fn2(1) // ok
    

对象内使用函数类型

interface Obj {
    fn: (a: number) => void;
    // 也可以这样写
    // fn(a: number): void;
}
const obj: Obj = {
    fn: (a) => {
        console.log(a);
    },
    // 也可以这样写
    // fn(a) {
    //     console.log(a);
    // },
};
obj.fn(99);

2、调用签名

JavaScript中,函数除了可调用之外,还可以具有属性,如:

function fn() {
   return 99
}
fn.age = 1 // 在函数中写入属性age
console.log(fn.age, fn()); // 1 99

然而,函数类型表达式的语法不允许声明属性,如果想声明函数的属性的类型,可以在一个对象类型中写一个调用签名

type FnType = {
    age: number;
    (param: number): number;
};

function getFnAge(fn: FnType) {
    console.log(fn.age, fn(99));
}

function fn(a: number) {
    return a;
}
fn.age = 18;

getFnAge(fn); // 18 99

注意:与函数类型表达式相比,语法略有不同:在参数列表和返回类型之间使用: 而不是=>

FnType也可以使用接口声明:

interface FnType {
    age: number;
    (param: number): number;
}

3、构造签名

JavaScript中存在一种使用new操作符调用的构造函数

// Fn就是一个构造函数

// ES5写法
// function Fn(age) {
//   this.age = age
// }

// ES6可以这么写
class Fn {
	// 添加构造函数(构造器)
    constructor(age) {
        this.age = age
    }
}

const f = new Fn(18)
console.log(f.age); // 18

new关键字来调用的函数,都称为构造函数,构造函数首字母一般大写,其作用是在创建对象的时候用来初始化对象,就是给对象成员赋初始值

ES6class 为构造函数的语法糖,即 class 的本质是构造函数。class的继承 extends 本质为构造函数的原型链的继承。

TypeScript中可以通过在调用签名前面添加new关键字来写一个构造签名

class Fn {
    age: number;
    constructor(age: number) {
        this.age = age;
    }
}

// 可以使用接口这么写
// interface FnType {
//     new (param: number): Fn; // 构造签名
// }

type FnType = new (param: number) => Fn;

function getFnAge(fn: FnType) {
    const f = new fn(18); // f类型为Fn
    console.log(f.age); // 18
}

getFnAge(Fn);

类型FnType 代表的是一个实例类型为Fn(或包含Fn)的构造函数,即classFn或其子类:

【TypeScript】深入学习TypeScript函数_第1张图片

  • 实例:即new出的结果,如上面的f
  • 构造签名中的返回值类型为类名
  • 从这里可以看出class类可以直接作为类型使用

有些对象,如 JavaScriptDate 对象,可以在有 new 或没有 new 的情况下被调用。你可以在同一类型中任意地结合调用和构造签名:

interface CallOrConstruct {
    new (s: string): Date;
    (): string;
}
function fn(date: CallOrConstruct) {
    let d = new date("2022-7-28");
    console.log(d); // 2022-07-27T16:00:00.000Z

    let n = date();
    console.log(n); // Thu Jul 28 2022 15:25:08 GMT+0800 (中国标准时间)
}
fn(Date);

4、泛型函数(通用函数)

TypeScript中,当我们想描述两个值之间的对应关系时,会使用泛型

泛型就是把两个或多个具有相同类型的值联系起来

在【TypeScript】深入学习TypeScript对象类型中我们提到了使用泛型对象类型实现通用函数,这其实就是泛型函数的使用,这里再看一个简单的例子:

在写一个函数时,输入的类型与输出的类型有关,或者两个输入的类型以某种方式相关,这是常见的。让我们考虑一下一个返回数组中第一个元素的函数:

function getFirstElement(arr: any[]) {
    return arr[0];
}

这个函数完成了它的工作,但不幸的是它的返回类型是 any ,如果该函数能够返回具体的类型会更好, 通过在函数签名中声明一个类型参数来做到这一点:

// 在函数签名中声明一个类型参数
function getFirstElement<Type>(arr: Type[]): Type | undefined {
    return arr[0];
}

// s 是 'string' 类型
const s = getFirstElement(["a", "b", "c"]);
// n 是 'number' 类型
const n = getFirstElement([1, 2, 3]);
// u 是 undefined 类型
const u = getFirstElement([]);

这样我们就在函数的输入(数组)和输出(返回值)之间建立了一个联系

类型推断

上面这个例子中,在我们使用getFirstElement函数时并没有指定类型,类型是由TypeScript自动推断并选择出来的

我们也可以使用多个类型参数

// 实现一个独立版本的map
function map<Input, Output>(
    arr: Input[],
    func: (arg: Input) => Output
): Output[] {
    return arr.map(func);
}

// 参数n的类型自动推断为字符串类型
// numArr类型自动推断为number[]
const numArr = map(["1", "2", "3"], (n) => parseInt(n));
console.log(numArr); // [1,2,3]

在这个例子中,TypeScript可以推断出输入类型参数的类型(从给定的字符串数组),以及基于函数表达式的返回值(数字)的输出类型参数。

指定类型参数

上面说到TypeScript可以自动推断出通用函数(泛型函数)调用中的类型参数,但这并不适用于所有情景,例如:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
    return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]);

上面我们实现了一个合并数组的函数,看上去它好像没什么问题,但实际上TypeScript已经抛出了错误:

【TypeScript】深入学习TypeScript函数_第2张图片
这时我们就可以手动指定类型参数,告诉TS这俩类型都是合法的:

const arr = combine<number | string>([1, 2, 3], ["hello"]);

限制条件

我们可以使用一个约束条件来限制一个类型参数可以接受的类型

让我们写一个函数,返回两个值中较长的值。要做到这一点,我们需要一个长度属性(类型为number)。我们可以通过写一个扩展子句extends将类型参数限制在这个类型上:

function getLong<Type extends { length: number }>(a: Type, b: Type) {
    if (a.length >= b.length) {
        return a;
    } else {
        return b;
    }
}
// longerArray 的类型是 'number[]'
const longerArray = getLong([1, 2], [1, 2, 3]);

// longerString 是 'alice'|'bob' 的类型。
const longerString = getLong("alice", "bob");

const obj1 = {
    name: "obj1",
    length: 9,
};
const obj2 = {
    name: "obj2",
    length: 5,
};
// longerObj 是 { name: string;length: number;} 的类型。
const longerObj = getLong(obj1, obj2);

// 错误! 数字没有'长度'属性
const notOK = getLong(10, 100); // err:类型“number”的参数不能赋给类型“{ length: number; }”的参数。

Type extends { length: number }就是说类型参数Type只能接收含有类型为number的属性length的类型

这个例子中我们并没有给getLong函数指定返回值类型,但TypeScript依旧能够推断出返回值类型

编写规范

  • 类型参数下推

    规则: 可能的情况下,使用类型参数本身,而不是对其进行约束

    // 推荐✅✅✅
    function firstElement1<Type>(arr: Type[]) {
        return arr[0];
    }
    // a类型为number 
    const a = firstElement1([1, 2, 3]);
    
    // 不推荐❌❌❌
    function firstElement2<Type extends any[]>(arr: Type) {
        return arr[0];
    }
    // b类型为any 
    const b = firstElement2([1, 2, 3]);
    
  • 使用更少的类型参数

    规则: 总是尽可能少地使用类型参数

    // 推荐✅✅✅
    function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
        return arr.filter(func);
    }
    const arr1 = filter1([1, 2, 3], (n) => n === 1);
    
    // 不推荐❌❌❌
    function filter2<Type, Func extends (arg: Type) => boolean>(
        arr: Type[],
        func: Func
    ): Type[] {
        return arr.filter(func);
    }
    // 这种写法,在想要手动指定参数时必须要指定两个,多次一举
    const arr2 = filter2<number, (arg: number) => boolean>(
        [1, 2, 3],
        (n) => n === 1
    );
    
  • 类型参数应出现两次

    规则: 如果一个类型的参数只出现在一个地方,请重新考虑你是否真的需要它

    // 推荐✅✅✅
    function greet(s: string) {
        console.log("Hello, " + s);
    }
    
    // 不推荐❌❌❌
    function greet<Str extends string>(s: Str) {
        console.log("Hello, " + s);
    }
    

5、可选参数

在博主TypeScript专栏的前几篇文章中我们多次提到过可选属性,这里就不过多叙述了,直接放代码:

// n为可选参数,它的类型为number|undefined
function fn(n?: number) {
    if (n) { // 操作可选参数之前一定要先判断其是否存在
        console.log(n + 1);
        return
    }
    console.log("未传参数");
}

fn(); // '未传参数'
fn(1); // 2
// 当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个 "丢失 "的参数
fn(undefined); // '未传参数' (与fn()效果相同)

也可以使用默认值:

function fn(n: number = 1) {
    if (n) {
        console.log(n + 1);
        return;
    }
    console.log("未传参数");
}

fn(); // 2
fn(1); // 2
// 当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个 "丢失 "的参数
fn(undefined); // 2  (与fn()效果相同)

6、函数重载

有时我们需要以不同的方式(传递数量不同的参数)来调用函数,但是我们调用的方式是有限的,这时如果是使用可选参数就会出现问题

例如我们希望一个函数只能接收一个参数或三个参数,不能接收其它数量的参数,我们尝试使用可选参数来实现:

function fn(a: number, b?: number, c?: number) {}

fn(1);
fn(1, 2, 3);
fn(1, 2); // 并不会报错

可以看到我们可以给函数传递两个参数,这显然不符合我们的需求,这种情况下我们就可以通过编写重载签名来指定调用函数的不同方式:

// 重载签名
function fn(a: number): void; // 接收一个参数的签名
function fn(a: number, b: number, c: number): void; // 接收三个参数的签名
// 实现签名(函数主体)
function fn(a: number, b?: number, c?: number) {}

【TypeScript】深入学习TypeScript函数_第3张图片

这里有几种重载签名,函数就有几种方式调用

可以看到这完美实现了我们的需求!

上述使用重载签名实现签名共同组合定义的函数fn就是一个重载函数,接下来我们深入探讨重载签名与实现签名:

重载签名与实现签名

实现签名就是函数的主体,一个普通的函数,这里就不多说了

重载签名格式:function FnName(param: Type): returnType

  • FnName:函数的名称,必须与实现签名(即函数的主体)的名称相同
  • 其余部分与函数类型表达式大致相同:Type为参数param的类型,returnType为函数返回值类型

注意事项:

  • 重载签名必须要在实现签名的上边:

    在这里插入图片描述

  • 调用重载函数所传的参数数量必须是定义的重载签名的一种,即使函数主体没有声明形参:

    【TypeScript】深入学习TypeScript函数_第4张图片

  • 重载签名必须与实现签名兼容:

    【TypeScript】深入学习TypeScript函数_第5张图片

    【TypeScript】深入学习TypeScript函数_第6张图片

编写规范

  • 当重载签名有相同的参数数量时,不推荐使用重载函数

如我们编写一个返回字符串或数组长度的重载函数:

function fn(x: string): number;
function fn(x: any[]): number;
function fn(x: string | any[]) {
    return x.length;
}

这个函数是好的,我们可以用字符串或数组来调用它。

然而,我们不能用一个即可能是字符串又可能是数组的值来调用它,因为TypeScript只能将一个函数调用解析为一个重载:

【TypeScript】深入学习TypeScript函数_第7张图片
这里两个重载签名具有相同的参数数量和返回类型,我们完全可以改写一个非重载版本的函数:

function fn(x: string | any[]) {
    return x.length;
}

fn("Ailjx");
fn([1, 2]);
// 报错
fn(Math.random() > 0.5 ? "Ailjx" : [1, 2]);

这样即避免了报错,又使代码变得更加简洁,这时你就会发现那两行重载签名是多么的没用,所以在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数

在函数中声明this

TypeScript 将通过代码流分析推断this在函数中应该是什么,例如:

interface User {
    name: string;
    setName: (newName: string) => void;
}
const user: User = {
    name: "Ailjx",
    setName: function (newName: string) {
        this.name = newName;
    },
};

一般情况下这已经足够了,但是在一些情况下,您可能需要更多地控制this对象代表的内容

JavaScript 规范声明你不能有一个名为this的参数,因此 TypeScript 使用该语法空间让你能够在函数体中声明this的类型:

interface User {
    name: string;
    setName: (newName: string) => void;
}
const user: User = {
    name: "Ailjx",
    // 手动声明this的类型为User
    setName: function (this: User, newName: string) {
        this.name = newName;
    },
};

上面我们在函数的参数中加上了this:User,指定了this的类型为User,这里的this代表的并不是形参(因为JavaScriptthis不能作为形参),在编译后的JavaScript代码中它会自动去除掉:

// 上述代码编译后的JS
"use strict";
const user = {
    name: "Ailjx",
    setName: function (newName) {
        this.name = newName;
    },
};

注意:

  • this类型的声明需要在函数的第一个参数的位置上

  • 不能在箭头函数中声明this类型

    【TypeScript】深入学习TypeScript函数_第8张图片

7、参数展开运算符

形参展开

JavaScript中一样,rest 参数出现在所有其他参数之后,并使用... 的语法:

function multiply(n: number, ...m: number[]) {
    return m.map((x) => n * x);
}

const a = multiply(10, 1, 2, 3, 4); //  [10, 20, 30, 40]

rest 参数的类型默认是any[]

实参展开

在使用push方法时使用实参展开:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
console.log(arr1); // [1,2,3,4,5,6]

在一些情况下,直接进行实参展开我们会遇到问题,如:
在这里插入图片描述

Math.atan2(y,x)返回从原点 (0,0)(x,y) 点的线段与 x 轴正方向之间的平面角度 (弧度值),点击查看详情

最直接的解决方案是使用as const文字断言:

const args = [8, 5] as const;
const angle = Math.atan2(...args);

8、参数解构

对于这样的函数:

type Num={ a: number; b: number; c: number }
function sum(num: Num) {
	console.log(num.a + num.b + num.c);
}

可以使用解构语法:

type Num={ a: number; b: number; c: number }
function sum({ a, b, c }: Num) {
	console.log(a + b + c);
}

9、函数的可分配性

一个具有 void 返回类型的上下文函数类型( () => void ),在实现时,可以返回任何其他的值,但这些返回值的类型依旧是void

type voidFunc = () => void;

const f1: voidFunc = () => {
    return 1;
};

const f2: voidFunc = () => 2;

const f3: voidFunc = function () {
    return 3;
};

// v1,v2,v3的类型都是void
const v1 = f1();
const v2 = f2();
const v3 = f3();

console.log(v1, v2, v3); // 1 2 3

这种行为使得下面的代码是有效的:

const arr = [1, 2, 3];
const num = [];
arr.forEach((el) => num.push(el));

即使 push方法返回值是一个数字,而forEach 方法期望得到一个返回类型为void 的函数,因为上面分析的原因,它们依旧可以组合在一起

需要注意的是,当一个字面的函数定义有一个 void 的返回类型时,该函数必须不返回任何东西:

【TypeScript】深入学习TypeScript函数_第9张图片

结语

博主的TypeScript专栏正在慢慢的补充之中,赶快关注订阅,与博主一起进步吧!期待你的三连支持。

参考资料:TypeScript官网

你可能感兴趣的:(typescript,学习,javascript)