# 三、函数
🚀【TypeScript入门手册】记录了出场率较高的Ts概念,旨在帮助大家了解并熟悉Ts
🎉 本系列会持续更新并更正,重点关照大家感兴趣的点,欢迎同学留言交流,在进阶之路上,共勉!
👍 star本项目给作者一点鼓励吧
📚 系列文章,收藏 不走丢哦
# 1.函数声明
函数是一等公民,在JavaScript中,有两种常见的定义函数的方式——函数声明和函数表达式,让我们来学习一下如何书写描述函数的类型(types)。
// 函数声明(Function Declaration)
function sum(x, y) {
return x + y;
}
// 函数表达式(Function Expression)
const mySum = function(x, y) {
return x + y;
};
# 1.1 函数声明
一个函数有输入和输出,要在 TypeScript 中对其进行约束,两者都要考虑到,其中函数声明的类型定义较简单:
// (local function) string2Number(s: string): number
function string2Number(s: string): number {
return Number(s); // s: string
}
传入不合类型的参数,是不被允许的:
// (local function) string2Number(s: string): number
function string2Number(s: string): number {
return Number(s); // s: string
}
string2Number(100); // 类型“number”的参数不能赋给类型“string”的参数。
string2Number('100', 200); // 应有 1 个参数,但获得 2 个。
# 1.2 函数表达式
最简单的描述函数的方式就是函数类型表达式,应该是怎么样的呢?
let mySum1 = function(x: number, y: number): number {
return x + y;
};
// 这里如果不写mySum1的返回值类型,ts一样可以推断出来
这段代码不会报错,也是正确的,而还有一种函数表达式的定义是这样的,这样的写法一直让我觉得怪异,毕竟和箭头函数太相似了
let mySum2: (x: number, y: number) => number = function(
x: number,
y: number
): number {
return x + y;
};
两者的区别是 mySum1 的类型是推导出来的,而 mySum2 是直接定义的,但两者其实一样,不需要深究。
# 2.函数签名
在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的。然而上一节讲到的函数类型表达式并不能支持声明属性,如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature)。
type DescribableFunction = {
description: string; // 函数的属性
test1: string,
(someArg: number): boolean; // 参数列表和返回值类型
};
function doSomething(fn: DescribableFunction) {
console.log(fn.test1); // test1: string
console.log(fn.description); // description: string
fn('1'); // 类型“string”的参数不能赋给类型“number”的参数。
}
# 3.泛型函数
- 指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。记住
- 所谓泛型就是用一个相同类型来关联两个或者更多的值。
我们经常需要写这种函数,即函数的输出类型依赖函数的输入类型,或者两个输入的类型以某种形式相互关联。让我们考虑这样一个函数,它返回数组的第一个元素:
function firstElement(arr: any[]) {
return arr[0];
}
注意此时函数返回值的类型是 any,如果能返回第一个元素的具体类型就更好了。
此时我们需要在函数签名里声明一个类型参数 (type parameter):
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const str = firstElement(['str']);
// const str: string
const bool = firstElement([true]);
// const bool: boolean
const num = firstElement([1]);
// const num: number
通过给函数添加一个类型参数 T
,并且在两个地方使用它,我们就在函数的输入(即数组)和函数的输出(即返回值)之间创建了一个关联。现在当我们调用它,一个更具体的类型就会被判断出来:
# 3.1 推断(Inference)
注意在上面的例子中,我们没有明确指定T
的类型,类型是被 TypeScript 自动推断出来的。
我们也可以使用多个类型参数,举个例子:
const res = map([1, 2, 3, 4], (val) => val.split(","));
// (parameter) val: number
// 类型“number”上不存在属性“split”。
注意在这个例子中,TypeScript 既可以推断出 Input 的类型是number ,此时自然不能使用split方法。
# 3.2 约束(Constraints)
有的时候,我们想关联两个值,但只能操作值的一些固定字段,这种情况,我们可以使用**约束(constraint)**对类型参数进行限制。
让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个number类型的length属性。我们使用extends
语法来约束函数参数:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
const res1 = longest([1, 2, 3], 1);
// ❌ 类型“number”的参数不能赋给类型“{ length: number; }”的参数。
const res2 = longest([1, "2", 3], [1]);
// const res2: (string | number)[]
const res3 = longest('yuguang', '余光');
// const res3: "yuguang" | "余光"
正是因为我们对Type做了{ length: number }
限制,我们才可以被允许获取 a b参数的 .length 属性。没有这个类型约束,我们甚至不能获取这些属性,因为这些值也许是其他类型,并没有 length 属性。
基于传入的参数,longerArray和 longerString 中的类型都被推断出来了。
注意:这是一个使用泛型约束常出现的错误:
function minimumLength<T extends { length: number }>(
obj: T,
minimum: number
): T {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
// 不能将类型“{ length: number; }”分配给类型“T”。
// "{ length: number; }" 可赋给 "T" 类型的约束,但可以使用约束 "{ length: number; }" 的其他子类型实例化 "T"。
}
}
这里 T 被约束后,函数返回了一个符合 T 约束的对象,{length: 1, name: 1}
符合约束,所以函数要返回的是类型一样的对象~
# 3.3 声明类型参数 (Specifying Type Arguments)
TypeScript 通常能自动推断泛型调用中传入的类型参数,但也并不能总是推断出。举个例子,有这样一个合并两个数组的函数:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
// 如果你像下面这样调用函数就会出现错误:
const arr1 = combine([1, 2, 3], ["hello"]);
// 不能将类型“string”分配给类型“number”
// 而如果你执意要这样做,你可以手动指定 Type:
const arr2 = combine<string | number>([1, 2, 3], ["hello"]);
# 4.函数参数
# 4.1 可选参数
如果你已经对函数的参数类型声明,那么输入多余的(或者少于要求的)参数,是不允许的。与接口中的可选属性类似,我们用?
表示可选的参数:
// bad
function hello(name1: string, name2: string): void {
console.log(`hello! ${name1} ${name2 ? "and" + " " + name2 : ""}`);
}
hello("余光"); // 应有 2 个参数,但获得 1 个, 未提供 "name2" 的自变量。
// good
function hello1(name1: string, name2?: string): void {
console.log(`hello! ${name1} ${name2 ? "and" + " " + name2 : ""}`);
}
hello1("余光");
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
# 4.2 参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
function hello(name1: string = "余光", name2: string = "yuguang"): void {
console.log(`hello! ${name1} ${name2 ? "and" + " " + name2 : ""}`);
}
hello(); // hello! 余光 and yuguang
hello("小明"); // hello! 小明 and yuguang
# 4.3 剩余参数
ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):
function func(a, ...arg) {
console.log(a);
console.log(arg);
}
func(1, 2, 3, 4); // 1, [2, 3, 4]
**注意:**rest参数只能是最后一个参数
# 4.4 参数解构
你可以使用参数解构方便的将作为参数提供的对象解构为函数体内一个或者多个局部变量,在 JavaScript 中,是这样的:
interface Man { name: string; age: number };
function sum({ name, age }: Man) {
console.log(name + age);
}
sum({ a: 10, b: 3, c: 9 });
// 类型“{ a: number; b: number; c: number; }”的参数不能赋给类型“Man”的参数。
// 对象文字可以只指定已知属性,并且“a”不在类型“Man”中。ts
# 5.其他类型
这里介绍一些也会经常出现的类型。像其他的类型一样,你也可以在任何地方使用它们,但它们经常与函数搭配使用。
# 5.1 void
void 表示一个函数并不会返回任何值,当函数并没有任何返回值,或者返回不了明确的值的时候,就应该用这种类型。
// (local function) test(): void
function test() {
console.log('hello world');
}
在 JavaScript 中,一个函数并不会返回任何值,会隐式返回 undefined,但是 void 和 undefined 在 TypeScript 中并不一样。
# 5.2 unknown
unknown
类型可以表示任何值。有点类似于 any,但是更安全,因为对 unknown 类型的值做任何事情都是不合法的:
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// 类型“unknown”上不存在属性“b”
}
有的时候用来描述函数类型,还是蛮有用的。比如:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
# 5.3 never
一些函数从来不返回值:
function fail(msg: string): never {
throw new Error(msg);
}
never
类型表示一个值不会再被观察到 (observed)。
作为一个返回类型时,它表示这个函数会丢一个异常,或者会结束程序的执行。
当 TypeScript 确定在联合类型中已经没有可能是其中的类型的时候,never
类型也会出现:
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
# 写在最后
参考:
欢迎Star⭐️