0
点赞
收藏
分享

微信扫一扫

【TypeScript】深入学习TypeScript函数


目录

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

前言

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

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

1、函数类型表达式

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

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

声明一个函数类型​​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

构造函数就是你构造出来的函数,是一种特殊的方法(不特指某一具体的内容,​​ES5​​​和​​ES6​​中的构造函数写法就不同),与普通函数有着质的区别,其作用是在创建对象的时候用来初始化对象,就是给对象成员赋初始值,构造函数的主要特征就是方法名首字母大写,并且用​​new​​来调用

在​​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);
console.log(f.age); // 18
}

getFnAge(Fn);

这里需要注意构造签名的的返回值类型为类名​Fn​

有些对象,如 ​​JavaScript​​​ 的 ​​Date​​​ 对象,可以在有 ​​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函数_类型参数


这时我们就可以手动指定类型参数,告诉​​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函数_学习_02

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

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

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

重载签名与实现签名

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

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

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

注意事项:

  • 重载签名必须要在实现签名的上边:
  • 【TypeScript】深入学习TypeScript函数_typescript_03

  • 调用重载函数所传的参数数量必须是定义的重载签名的一种,即使函数主体没有声明形参:
  • 【TypeScript】深入学习TypeScript函数_学习_04

  • 重载签名必须与实现签名兼容:
  • 【TypeScript】深入学习TypeScript函数_学习_05


  • 【TypeScript】深入学习TypeScript函数_typescript_06

编写规范

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

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

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

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

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

【TypeScript】深入学习TypeScript函数_学习_07


这里两个重载签名具有相同的参数数量和返回类型,我们完全可以改写一个非重载版本的函数:

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​​​代表的并不是形参(因为​​JavaScript​​​中​​this​​​不能作为形参),在编译后的​​JavaScript​​代码中它会自动去除掉:

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

注意:

  • ​this​​类型的声明需要在函数的第一个参数的位置上
  • 不能在箭头函数中声明​​this​​类型
  • 【TypeScript】深入学习TypeScript函数_类型参数_08

7、需要了解的其它类型

void

​void​​表示不返回值的函数的返回值。

只要函数没有任何​​return​​​语句,或者没有从这些返回语句中返回任何显式值,它的推断类型就是​​void​

在​​JavaScript​​​中,一个不返回任何值的函数将隐含地返回​​undefinded​​ 的值。

然而,在​​TypeScript​​​中,​​void​​​ 和​​undefined​​ 是不一样的。

object

特殊类型 ​​object​​​ 指的是任何不是基元的值( ​​string​​​ 、​​number​​​ 、​​bigint​​​ 、​​boolean​​​ 、​​symbol​​​ 、​​null​​​ 或 ​​undefined​​ )。

这与空对象类型​​{ }​​​ 不同,也与全局类型 ​​Object​​​ (大写的​​O​​​)不同, ​​Object​​​ 类型一般是永远也用不上,使用的都是​​object​

请注意,在​​JavaScript​​​中,函数值是对象。它们有属性,在它们的原型链中有​​Object.prototype​​​ ,是​​Object​​​ 的实例,你可以对它们调用 ​​Object.key​​ 等等。

由于这个原因,函数类型在TypeScript中被认为是​object​ !

unknown

在【TypeScript】TypeScript常用类型(上篇)​中已经介绍过​​unknown​​,这里就不再多说了

never

见【TypeScript】TypeScript中类型缩小(含类型保护)与类型谓词中never 类型与穷尽性检查

Function

全局类型​​Function​​​描述了 ​​JavaScript​​​ 中所有函数值上的属性,如​​bind​​​、​​call​​​、​​apply​​和其他属性。

它还有一个特殊的属性,即 ​​Function​​​ 类型的值总是可以被调用,这些调用返回 ​​any​

function doSomething(f: Function) {
return f(1, 2, 3);
}

这是一个无类型的函数调用,一般来说最好避免,因为 ​​any​​ 返回类型都不安全。

如果你需要接受一个任意的函数,但不打算调用它,一般来说 ​​() => void​​ 的类型比较安全。

8、参数展开运算符

形参展开

和​​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]

在一些情况下,直接进行实参展开我们会遇到问题,如:

【TypeScript】深入学习TypeScript函数_类型参数_09

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

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

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

9、参数解构

对于这样的函数:

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);
}

10、函数的可分配性

一个具有 ​​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函数_javascript_10

结语

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

参考资料:​​TypeScript官网​​


举报

相关推荐

0 条评论