最近博主一直在创作TypeScript
的内容,所有的TypeScript
文章都在我的TypeScript专栏里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅
今天呢,我们将深入去学习TypeScript
中的函数
函数类型格式为: (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);
在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;
}
在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
关键字来调用的函数,都称为构造函数,构造函数首字母一般大写,其作用是在创建对象的时候用来初始化对象,就是给对象成员赋初始值
ES6
的class
为构造函数的语法糖,即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
)的构造函数,即class
类Fn
或其子类:
- 实例:即
new
出的结果,如上面的f
- 构造签名中的返回值类型为类名
- 从这里可以看出
class
类可以直接作为类型使用
有些对象,如 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);
在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
已经抛出了错误:
这时我们就可以手动指定类型参数,告诉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);
}
在博主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()效果相同)
有时我们需要以不同的方式(传递数量不同的参数)来调用函数,但是我们调用的方式是有限的,这时如果是使用可选参数就会出现问题
例如我们希望一个函数只能接收一个参数或三个参数,不能接收其它数量的参数,我们尝试使用可选参数来实现:
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) {}
这里有几种重载签名,函数就有几种方式调用
可以看到这完美实现了我们的需求!
上述使用重载签名与实现签名共同组合定义的函数fn
就是一个重载函数,接下来我们深入探讨重载签名与实现签名:
实现签名就是函数的主体,一个普通的函数,这里就不多说了
重载签名格式:function FnName(param: Type): returnType
FnName
:函数的名称,必须与实现签名(即函数的主体)的名称相同Type
为参数param
的类型,returnType
为函数返回值类型注意事项:
如我们编写一个返回字符串或数组长度的重载函数:
function fn(x: string): number;
function fn(x: any[]): number;
function fn(x: string | any[]) {
return x.length;
}
这个函数是好的,我们可以用字符串或数组来调用它。
然而,我们不能用一个即可能是字符串又可能是数组的值来调用它,因为TypeScript
只能将一个函数调用解析为一个重载:
这里两个重载签名具有相同的参数数量和返回类型,我们完全可以改写一个非重载版本的函数:
function fn(x: string | any[]) {
return x.length;
}
fn("Ailjx");
fn([1, 2]);
// 报错
fn(Math.random() > 0.5 ? "Ailjx" : [1, 2]);
这样即避免了报错,又使代码变得更加简洁,这时你就会发现那两行重载签名是多么的没用,所以在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数
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;
},
};
注意:
和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);
对于这样的函数:
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);
}
一个具有 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官网