MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript函数定义与类型注解详解

2021-07-221.2k 阅读

函数定义基础

在TypeScript中,函数定义与JavaScript有相似之处,但TypeScript通过类型注解提供了更严格的类型检查。函数定义主要由函数名、参数列表、函数体和返回值组成。

基本函数定义

function add(a: number, b: number): number {
    return a + b;
}

在上述代码中,add 是函数名,ab 是参数,number 是参数的类型注解,表示 ab 都必须是数字类型。函数体执行加法运算,并返回一个 number 类型的值。

无参数函数

function greet(): string {
    return "Hello, world!";
}

这个 greet 函数没有参数,返回一个字符串类型的值。

无返回值函数

function printMessage(message: string): void {
    console.log(message);
}

printMessage 函数接受一个字符串参数,并将其打印到控制台,但不返回任何值,因此返回类型为 void

函数参数类型注解

必选参数

在前面的例子中,add 函数的 ab 参数都是必选参数。当调用 add 函数时,必须提供两个数字参数,否则会报错。

// 正确调用
add(1, 2); 

// 错误调用,参数数量不足
add(1); 

// 错误调用,参数类型错误
add('1', '2'); 

可选参数

有时候,我们希望某些参数是可选的。在TypeScript中,可以通过在参数名后加 ? 来表示可选参数。

function greetWithName(name?: string): string {
    if (name) {
        return `Hello, ${name}!`;
    } else {
        return "Hello, stranger!";
    }
}

greetWithName 函数的 name 参数是可选的。可以这样调用该函数:

greetWithName(); 
greetWithName('John'); 

默认参数

TypeScript还支持为参数提供默认值。

function multiply(a: number, b: number = 2): number {
    return a * b;
}

multiply 函数中,b 参数有默认值 2。调用时可以只提供 a 参数:

multiply(5); 
multiply(5, 3); 

剩余参数

当函数需要接受不定数量的参数时,可以使用剩余参数。剩余参数使用 ... 语法,并且必须放在参数列表的最后。

function sum(...numbers: number[]): number {
    return numbers.reduce((acc, num) => acc + num, 0);
}

sum 函数接受任意数量的数字参数,并返回它们的总和。可以这样调用:

sum(1, 2, 3); 
sum(4, 5); 

函数返回值类型注解

返回值类型推断

在很多情况下,TypeScript可以根据函数体的返回语句推断出返回值类型。

function square(x: number) {
    return x * x;
}

虽然没有显式指定返回值类型,但TypeScript能推断出 square 函数返回一个 number 类型的值。

显式返回值类型注解

然而,在某些复杂情况下,显式指定返回值类型可以使代码更清晰,也有助于避免错误。

function getResult(data: string | number): string {
    if (typeof data === 'number') {
        return data.toString();
    } else {
        return data;
    }
}

这里,getResult 函数接受一个 stringnumber 类型的参数,并始终返回一个 string 类型的值。显式的返回值类型注解使代码意图更明确。

函数类型定义

类型别名表示函数类型

可以使用类型别名来定义函数类型,这在将函数作为参数传递或返回值时非常有用。

type Adder = (a: number, b: number) => number;
function useAdder(adder: Adder): number {
    return adder(3, 4);
}

在上述代码中,Adder 是一个函数类型别名,表示接受两个 number 类型参数并返回一个 number 类型值的函数。useAdder 函数接受一个 Adder 类型的函数作为参数并调用它。

接口表示函数类型

接口也可以用来定义函数类型。

interface MathOperation {
    (a: number, b: number): number;
}
function performOperation(operation: MathOperation): number {
    return operation(5, 6);
}

MathOperation 接口定义了一个函数类型,performOperation 函数接受符合该接口定义的函数作为参数。

函数重载

什么是函数重载

函数重载允许一个函数根据不同的参数列表执行不同的逻辑。在TypeScript中,通过为同一个函数定义多个函数签名来实现函数重载。

function printValue(value: string): void;
function printValue(value: number): void;
function printValue(value: any) {
    if (typeof value ==='string') {
        console.log(`String: ${value}`);
    } else if (typeof value === 'number') {
        console.log(`Number: ${value}`);
    }
}

这里定义了两个函数签名 printValue(value: string): voidprintValue(value: number): void,实际的函数实现可以接受任何类型的参数,但会根据参数类型执行不同的逻辑。

调用重载函数

调用重载函数时,TypeScript会根据传入的参数类型选择合适的函数签名。

printValue('Hello'); 
printValue(42); 

函数的this类型

函数内部的this

在JavaScript中,this 的值在函数调用时根据调用上下文确定,这可能会导致一些难以调试的问题。TypeScript提供了对 this 类型的更精确控制。

const obj = {
    name: 'Alice',
    printName: function() {
        console.log(this.name);
    }
};
obj.printName(); 

在上述代码中,printName 函数内部的 this 指向 obj 对象。

显式指定this类型

有时候,我们需要显式指定函数内部 this 的类型。

interface Person {
    name: string;
}
function greet(this: Person) {
    console.log(`Hello, ${this.name}!`);
}
const person: Person = { name: 'Bob' };
greet.call(person); 

greet 函数定义中,显式指定 this 的类型为 Person 接口类型。调用时使用 call 方法确保 this 指向 person 对象。

箭头函数与this

箭头函数的this绑定

箭头函数没有自己的 this,它的 this 继承自外层作用域。

const outerThis = {
    value: 10,
    getInnerValue: function() {
        const innerFunction = () => {
            return this.value;
        };
        return innerFunction();
    }
};
console.log(outerThis.getInnerValue()); 

getInnerValue 方法内部的箭头函数 innerFunction 中,this 指向 outerThis 对象。

与普通函数this的区别

普通函数的 this 根据调用方式确定,而箭头函数的 this 取决于定义时的外层作用域。

function regularFunction() {
    console.log(this);
}
const arrowFunction = () => {
    console.log(this);
};
regularFunction(); 
arrowFunction(); 

在全局作用域下调用,普通函数 regularFunctionthis 指向全局对象(浏览器中是 window,Node.js 中是 global),而箭头函数 arrowFunctionthis 指向外层作用域(同样是全局对象)。但如果在对象方法中使用,差异会更明显。

const obj1 = {
    value: 20,
    regularMethod: function() {
        setTimeout(function() {
            console.log(this.value); 
        }, 1000);
    },
    arrowMethod: function() {
        setTimeout(() => {
            console.log(this.value); 
        }, 1000);
    }
};
obj1.regularMethod(); 
obj1.arrowMethod(); 

regularMethod 中,setTimeout 内的普通函数的 this 指向全局对象,所以 this.valueundefined。而在 arrowMethod 中,setTimeout 内的箭头函数的 this 继承自 arrowMethodthis,所以能正确打印出 20

函数类型兼容性

参数类型兼容性

在TypeScript中,函数参数类型的兼容性是双向的。如果一个函数需要的参数类型是 A,另一个函数提供的参数类型是 B,那么只要 B 能赋值给 A,就认为这两个函数的参数类型兼容。

type Fn1 = (a: number) => void;
type Fn2 = (a: number | string) => void;
let fn1: Fn1;
let fn2: Fn2;
fn1 = fn2; 
fn2 = fn1; 

这里,Fn1 的参数类型是 numberFn2 的参数类型是 number | string。因为 number 类型的值可以赋值给 number | string 类型,所以 fn1 = fn2 是允许的。同时,因为 number | string 类型的值在某些情况下(当值为 number 时)可以赋值给 number 类型,所以 fn2 = fn1 也是允许的。

返回值类型兼容性

返回值类型的兼容性是单向的。如果一个函数返回类型是 A,另一个函数返回类型是 B,那么只有当 B 能赋值给 A 时,才认为这两个函数的返回值类型兼容。

type Fn3 = () => number;
type Fn4 = () => number | string;
let fn3: Fn3;
let fn4: Fn4;
fn3 = fn4; 
// fn4 = fn3;  这行代码会报错,因为 number 类型不能完全兼容 number | string 类型

Fn3 返回 number 类型,Fn4 返回 number | string 类型。number | string 类型的值在某些情况下(当值为 number 时)可以赋值给 number 类型,所以 fn3 = fn4 是允许的。但反过来,number 类型不能完全覆盖 number | string 类型,所以 fn4 = fn3 会报错。

高阶函数

什么是高阶函数

高阶函数是指接受一个或多个函数作为参数,或者返回一个函数的函数。

function applyOperation(a: number, b: number, operation: (a: number, b: number) => number): number {
    return operation(a, b);
}
function addNumbers(a: number, b: number): number {
    return a + b;
}
function multiplyNumbers(a: number, b: number): number {
    return a * b;
}
const sum = applyOperation(3, 4, addNumbers); 
const product = applyOperation(3, 4, multiplyNumbers); 

在上述代码中,applyOperation 是一个高阶函数,它接受两个数字和一个操作函数作为参数,并返回操作结果。

高阶函数返回函数

高阶函数也可以返回一个函数。

function createAdder(x: number): (y: number) => number {
    return function(y: number) {
        return x + y;
    };
}
const add5 = createAdder(5);
const result = add5(3); 

createAdder 函数接受一个数字 x 并返回一个新的函数。新函数接受另一个数字 y 并返回 x + y 的结果。

泛型函数

泛型函数基础

泛型函数允许我们在定义函数时使用类型变量,使函数可以适用于多种类型。

function identity<T>(arg: T): T {
    return arg;
}
const result1 = identity<number>(5); 
const result2 = identity<string>('Hello'); 

identity 函数中,<T> 是类型变量,arg 参数和返回值的类型都是 T。调用时可以显式指定类型参数,如 identity<number>(5),也可以让TypeScript根据传入的参数推断类型,如 identity(5)

多个类型变量

泛型函数可以有多个类型变量。

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}
const swapped = swap([1, 'two']); 

swap 函数接受一个包含两个元素的元组,类型变量 TU 分别表示元组中两个元素的类型,函数返回一个交换了元素顺序的新元组。

泛型函数的约束

有时候,我们希望对泛型类型变量进行约束,使其满足一定的条件。

interface Lengthwise {
    length: number;
}
function printLength<T extends Lengthwise>(arg: T): void {
    console.log(arg.length);
}
printLength('Hello'); 
printLength([1, 2, 3]); 
// printLength(5);  这行代码会报错,因为 number 类型没有 length 属性

printLength 函数中,类型变量 T 被约束为必须包含 length 属性。这样,只有满足 Lengthwise 接口的类型才能作为参数传递给该函数。

函数类型与类型断言

类型断言用于函数

类型断言可以用于将一个值断言为特定的函数类型。

const value: any = 'Hello';
const printValue = value as (() => void);
printValue(); 

在上述代码中,value 初始类型为 any,通过类型断言将其转换为 () => void 类型的函数。但这样做有风险,如果 value 实际上不是一个函数,运行时会报错。

更安全的类型断言

为了更安全地进行类型断言,可以结合类型检查。

const value2: any = function() {
    console.log('I am a function');
};
if (typeof value2 === 'function') {
    const func = value2 as () => void;
    func(); 
}

这里先通过 typeof 检查 value2 是否为函数,然后再进行类型断言,这样可以避免运行时错误。

函数与模块

模块中导出函数

在TypeScript模块中,可以导出函数供其他模块使用。

// mathUtils.ts
export function add(a: number, b: number): number {
    return a + b;
}
export function subtract(a: number, b: number): number {
    return a - b;
}
// main.ts
import { add, subtract } from './mathUtils';
const sum = add(3, 4); 
const diff = subtract(5, 2); 

mathUtils.ts 模块中导出了 addsubtract 函数,在 main.ts 模块中通过 import 导入并使用这些函数。

默认导出函数

模块也可以有一个默认导出的函数。

// greeting.ts
const greet = (name: string) => `Hello, ${name}!`;
export default greet;
// app.ts
import greet from './greeting';
const message = greet('Alice'); 

greeting.ts 模块中,greet 函数被默认导出,在 app.ts 模块中使用 import... from 语法导入默认导出的函数。

通过对TypeScript函数定义与类型注解的详细解析,我们可以看到TypeScript为前端开发中的函数使用带来了更高的安全性和可维护性。从基本的函数定义到复杂的泛型函数、高阶函数,以及函数在模块中的使用,TypeScript提供了丰富的功能来满足各种开发需求。合理运用这些特性,可以让我们的前端代码更加健壮和易于理解。