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

TypeScript函数参数与返回值的类型定义

2022-03-021.8k 阅读

TypeScript函数参数类型定义基础

简单类型参数

在TypeScript中,为函数参数定义类型是确保代码健壮性的重要一步。最基础的,我们可以为参数定义简单类型。例如,当我们希望函数接收一个数字类型的参数时:

function addNumber(num: number): number {
    return num + 1;
}
let result = addNumber(5);
console.log(result); 

在上述代码中,addNumber函数定义了一个参数num,其类型为number。这样,当我们调用addNumber函数时,如果传入的不是数字类型,TypeScript编译器就会报错。比如:

// 这行代码会报错,因为'string' 类型不能赋值给 'number' 类型
let errorResult = addNumber('5'); 

同样的道理,对于字符串类型参数的函数定义如下:

function greet(name: string): string {
    return `Hello, ${name}!`;
}
let greeting = greet('Alice');
console.log(greeting); 

这里greet函数接收一个string类型的参数name,并返回一个问候语字符串。如果调用时传入非字符串类型,也会触发编译器错误。

可选参数

有时候,函数的某些参数不是必需的。在TypeScript中,我们可以通过在参数名后添加?来表示该参数是可选的。例如:

function printInfo(name: string, age?: number) {
    if (age) {
        console.log(`${name} is ${age} years old.`);
    } else {
        console.log(`${name}'s age is unknown.`);
    }
}
printInfo('Bob'); 
printInfo('Charlie', 30); 

printInfo函数中,age参数是可选的。我们在调用函数时,可以选择传入age参数,也可以不传入。需要注意的是,可选参数必须跟在必需参数之后。如果写成如下形式:

// 这是错误的写法,可选参数不能在必需参数之前
function wrongPrintInfo(age?: number, name: string) { 
    //...
}

TypeScript编译器会报错。

默认参数

除了可选参数,TypeScript还支持为参数设置默认值。当调用函数时没有传入该参数的值,就会使用默认值。例如:

function calculateArea(radius: number, pi = 3.14) {
    return pi * radius * radius;
}
let area1 = calculateArea(5); 
let area2 = calculateArea(5, 3.14159); 

calculateArea函数中,pi参数有默认值3.14。如果调用函数时没有传入pi的值,就会使用默认值3.14来计算面积;如果传入了pi的值,则使用传入的值进行计算。

剩余参数

当函数需要接收不定数量的参数时,我们可以使用剩余参数。剩余参数使用...语法,它会将传入的多个参数收集到一个数组中。例如:

function sumNumbers(...nums: number[]) {
    return nums.reduce((acc, num) => acc + num, 0);
}
let sum1 = sumNumbers(1, 2, 3); 
let sum2 = sumNumbers(10, 20); 

sumNumbers函数中,...nums表示剩余参数,它可以接收任意数量的number类型参数,并将这些参数组成一个数组。然后我们可以使用数组的方法,如reduce来对这些参数进行计算。

复杂类型参数类型定义

数组类型参数

在很多场景下,函数可能需要接收数组类型的参数。我们可以明确指定数组中元素的类型。例如,一个计算数组中所有数字之和的函数:

function sumArray(numbers: number[]): number {
    return numbers.reduce((acc, num) => acc + num, 0);
}
let numberArray = [1, 2, 3, 4];
let arraySum = sumArray(numberArray);
console.log(arraySum); 

这里sumArray函数接收一个number[]类型的参数numbers,即一个包含数字的数组。如果传入的数组中包含非数字类型的元素,TypeScript编译器会报错:

// 这行代码会报错,因为'string' 类型不能赋值给 'number' 类型
let wrongArray = [1, 'two']; 
let wrongSum = sumArray(wrongArray); 

除了简单的一维数组,对于多维数组,我们也可以进行类型定义。例如,定义一个二维数字数组:

function printMatrix(matrix: number[][]): void {
    matrix.forEach(row => {
        console.log(row.join(' '));
    });
}
let matrixArray: number[][] = [
    [1, 2, 3],
    [4, 5, 6]
];
printMatrix(matrixArray); 

printMatrix函数中,matrix参数的类型为number[][],表示一个二维数组,其中每个元素又是一个number类型的数组。

对象类型参数

函数也常常接收对象类型的参数。我们可以定义对象的形状,即对象包含哪些属性以及这些属性的类型。例如:

function printUser(user: { name: string; age: number }) {
    console.log(`${user.name} is ${user.age} years old.`);
}
let userObject = { name: 'David', age: 25 };
printUser(userObject); 

printUser函数中,user参数的类型被定义为一个对象,该对象必须包含name属性,类型为string,以及age属性,类型为number。如果传入的对象缺少这些属性或者属性类型不匹配,就会报错:

// 这行代码会报错,因为缺少 'age' 属性
let wrongUser1 = { name: 'Eve' }; 
printUser(wrongUser1); 

// 这行代码会报错,因为 'age' 属性类型不匹配
let wrongUser2 = { name: 'Frank', age: 'twenty' }; 
printUser(wrongUser2); 

我们还可以使用接口(interface)或类型别名(type)来更方便地定义对象类型。例如,使用接口定义User类型:

interface User {
    name: string;
    age: number;
}
function printUserWithInterface(user: User) {
    console.log(`${user.name} is ${user.age} years old.`);
}
let user1: User = { name: 'Grace', age: 30 };
printUserWithInterface(user1); 

使用类型别名定义User类型:

type UserType = {
    name: string;
    age: number;
};
function printUserWithTypeAlias(user: UserType) {
    console.log(`${user.name} is ${user.age} years old.`);
}
let user2: UserType = { name: 'Hank', age: 35 };
printUserWithTypeAlias(user2); 

联合类型参数

有时函数的参数可以是多种类型中的一种,这时我们可以使用联合类型。例如,一个函数既可以接收数字,也可以接收字符串:

function printValue(value: number | string) {
    if (typeof value === 'number') {
        console.log(`The number is ${value}`);
    } else {
        console.log(`The string is ${value}`);
    }
}
printValue(10); 
printValue('Hello'); 

printValue函数中,value参数的类型为number | string,表示它可以是数字类型或者字符串类型。在函数内部,我们通过typeof操作符来判断实际传入参数的类型,并进行相应的处理。

交叉类型参数

交叉类型用于将多个类型合并为一个类型。当函数的参数需要满足多个类型的要求时,可以使用交叉类型。例如,我们定义一个既包含name属性(字符串类型)又包含age属性(数字类型),同时还包含email属性(字符串类型)的对象类型:

interface NameAndAge {
    name: string;
    age: number;
}
interface Email {
    email: string;
}
function printPerson(person: NameAndAge & Email) {
    console.log(`${person.name} is ${person.age} years old and email is ${person.email}`);
}
let person1: NameAndAge & Email = { name: 'Ivy', age: 28, email: 'ivy@example.com' };
printPerson(person1); 

printPerson函数中,person参数的类型是NameAndAge & Email,即一个对象需要同时满足NameAndAge接口和Email接口的要求。

函数参数类型的高级特性

类型推断与显式类型声明

在很多情况下,TypeScript可以根据函数的使用方式自动推断出参数的类型,这就是类型推断。例如:

function multiply(a, b) {
    return a * b;
}
let product = multiply(2, 3); 

在上述代码中,虽然我们没有显式地为multiply函数的参数ab定义类型,但TypeScript可以根据调用时传入的参数23,推断出ab的类型为number。然而,在一些复杂的场景下,显式地声明参数类型可以使代码更加清晰,也有助于避免潜在的错误。比如:

function add<T>(a: T, b: T): T {
    // 这里假设T类型有 + 操作符,实际应用中可能需要更复杂的类型约束
    return a + b; 
}
let numAddResult = add(1, 2); 
let stringAddResult = add('hello', 'world'); 

在这个泛型函数add中,如果不显式地声明参数ab的类型为T,代码的意图就不那么清晰,并且可能在类型检查上出现问题。

函数重载

函数重载允许我们为同一个函数定义多个不同的签名,根据传入参数的不同类型或数量来调用不同的实现。例如,我们定义一个printValue函数,它既可以接收单个数字参数并打印,也可以接收两个数字参数并打印它们的和:

function printValue(value: number): void;
function printValue(value1: number, value2: number): void;
function printValue(value: any, value2?: any) {
    if (typeof value2 === 'number') {
        console.log(value + value2);
    } else {
        console.log(value);
    }
}
printValue(5); 
printValue(3, 4); 

在上述代码中,我们先定义了两个函数签名,一个接收单个number类型参数,另一个接收两个number类型参数。然后是实际的函数实现,它根据是否传入第二个参数来决定执行哪种逻辑。这样,通过函数重载,我们可以让同一个函数根据不同的参数情况执行不同的操作。

泛型参数

泛型是TypeScript的一个强大特性,它允许我们在定义函数时使用类型变量。类型变量就像一个占位符,在函数调用时才确定具体的类型。例如,定义一个用于获取数组第一个元素的泛型函数:

function getFirst<T>(array: T[]): T | undefined {
    return array.length > 0? array[0] : undefined;
}
let numberArray1 = [1, 2, 3];
let firstNumber = getFirst(numberArray1); 
let stringArray = ['a', 'b', 'c'];
let firstString = getFirst(stringArray); 

getFirst函数中,<T>表示定义了一个类型变量Tarray参数的类型为T[],表示一个元素类型为T的数组。返回值类型为T | undefined,表示可能返回数组的第一个元素(类型为T),如果数组为空则返回undefined。通过泛型,我们可以复用这个函数,而不必为不同类型的数组分别定义获取第一个元素的函数。

函数参数的逆变与协变

在TypeScript中,函数参数存在逆变(Contravariance)和协变(Covariance)的概念。以函数类型为例,当一个函数类型A的参数类型是另一个函数类型B的参数类型的超类型时,我们说函数类型A在参数位置是逆变的。例如:

interface Animal {
    name: string;
}
interface Dog extends Animal {
    breed: string;
}
function printAnimal(animal: Animal) {
    console.log(`Animal: ${animal.name}`);
}
function printDog(dog: Dog) {
    console.log(`Dog: ${dog.name}, Breed: ${dog.breed}`);
}
let animalPrinter: (animal: Animal) => void = printDog; 

在上述代码中,printDog函数的参数类型DogprintAnimal函数参数类型Animal的子类型,但是我们可以将printDog赋值给animalPrinter,这就是因为函数参数位置的逆变特性。而对于返回值类型,当一个函数类型A的返回值类型是另一个函数类型B的返回值类型的子类型时,我们说函数类型A在返回值位置是协变的。例如:

function createAnimal(): Animal {
    return { name: 'Generic Animal' };
}
function createDog(): Dog {
    return { name: 'Buddy', breed: 'Golden Retriever' };
}
let animalCreator: () => Animal = createDog; 

这里createDog函数的返回值类型DogcreateAnimal函数返回值类型Animal的子类型,我们可以将createDog赋值给animalCreator,这体现了返回值位置的协变特性。

TypeScript函数返回值类型定义

简单返回值类型

与参数类型定义类似,我们也需要为函数的返回值定义类型,以确保代码的正确性和可维护性。最简单的情况是返回一个简单类型,如数字、字符串等。例如,一个返回两个数字之和的函数:

function addNumbersReturn(num1: number, num2: number): number {
    return num1 + num2;
}
let sumResult = addNumbersReturn(5, 3);
console.log(sumResult); 

addNumbersReturn函数中,返回值类型被定义为number,表示该函数一定会返回一个数字。如果函数内部的返回值类型不符合这个定义,TypeScript编译器就会报错。比如:

function wrongReturn(): number {
    return 'not a number'; 
}

上述代码会报错,因为返回值类型是string,与定义的number类型不匹配。

复杂返回值类型

对象类型返回值

函数可以返回对象类型的值。我们同样需要定义返回对象的形状。例如,一个根据姓名和年龄创建用户对象的函数:

interface UserReturn {
    name: string;
    age: number;
}
function createUser(name: string, age: number): UserReturn {
    return { name, age };
}
let newUser = createUser('Leo', 40);
console.log(newUser); 

createUser函数中,返回值类型为UserReturn,这是一个通过接口定义的对象类型。函数返回的对象必须符合UserReturn接口的定义,即包含name属性(string类型)和age属性(number类型)。

数组类型返回值

有些函数会返回数组类型的值。例如,一个生成指定长度的数字数组的函数:

function generateNumberArray(length: number): number[] {
    let result: number[] = [];
    for (let i = 0; i < length; i++) {
        result.push(i);
    }
    return result;
}
let numberArray2 = generateNumberArray(5);
console.log(numberArray2); 

generateNumberArray函数中,返回值类型为number[],表示返回一个数字数组。

联合类型返回值

当函数可能返回多种类型的值时,可以使用联合类型定义返回值类型。例如,一个函数根据传入的条件返回数字或字符串:

function getValue(condition: boolean): number | string {
    if (condition) {
        return 10;
    } else {
        return 'default';
    }
}
let value1 = getValue(true); 
let value2 = getValue(false); 

getValue函数中,返回值类型为number | string,表示根据condition的值,函数可能返回数字类型的值,也可能返回字符串类型的值。

类型推断与显式返回值类型声明

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

function multiplyNumbers(a: number, b: number) {
    return a * b;
}
let multiplyResult = multiplyNumbers(2, 3); 

这里虽然没有显式地声明multiplyNumbers函数的返回值类型,但TypeScript根据return a * b推断出返回值类型为number。然而,在一些复杂的场景下,显式声明返回值类型可以提高代码的可读性和可维护性。比如,在使用递归的函数中:

function factorial(n: number): number {
    if (n === 0 || n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}
let factorialResult = factorial(5); 

factorial函数中,显式声明返回值类型为number,可以让阅读代码的人更清晰地了解函数的返回值情况,也有助于TypeScript进行更准确的类型检查。

函数返回值的泛型

泛型同样可以应用于函数的返回值类型。例如,一个将输入值原样返回的泛型函数:

function identity<T>(value: T): T {
    return value;
}
let numIdentity = identity(5); 
let stringIdentity = identity('hello'); 

identity函数中,<T>是类型变量,返回值类型为T,表示返回值类型与传入参数的类型相同。通过泛型,我们可以复用这个函数,使其适用于不同类型的值。

异步函数返回值类型

在TypeScript中,异步函数(使用async关键字定义的函数)的返回值类型比较特殊。异步函数总是返回一个Promise对象。例如:

async function fetchData(): Promise<string> {
    return 'data fetched';
}
fetchData().then(data => {
    console.log(data); 
});

fetchData函数中,返回值类型被定义为Promise<string>,表示该异步函数返回一个Promise,这个Promise解决(resolved)后的值为string类型。如果异步函数内部抛出错误,这个Promise会被拒绝(rejected)。我们可以使用try...catch块来处理可能的错误:

async function fetchError(): Promise<string> {
    throw new Error('fetch error');
}
fetchError().then(data => {
    console.log(data); 
}).catch(error => {
    console.error(error); 
});

这样,通过明确异步函数的返回值类型为Promise及其内部值的类型,我们可以更好地处理异步操作的结果和错误。

通过对TypeScript函数参数与返回值类型定义的深入理解和应用,我们能够编写出更健壮、可维护且类型安全的前端代码,有效地减少运行时错误,提高开发效率。在实际项目中,应根据具体需求合理选择和组合各种类型定义方式,以充分发挥TypeScript的优势。