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

TypeScript可选参数在回调函数中的使用技巧

2023-09-226.8k 阅读

TypeScript 可选参数基础

在深入探讨 TypeScript 可选参数在回调函数中的使用技巧之前,我们先来回顾一下 TypeScript 可选参数的基本概念。在 TypeScript 函数定义中,我们可以通过在参数名后添加问号 ? 来标记一个参数为可选参数。例如:

function greet(name: string, greeting?: string) {
    if (greeting) {
        return `${greeting}, ${name}!`;
    }
    return `Hello, ${name}!`;
}
console.log(greet('Alice'));
console.log(greet('Bob', 'Hi'));

在上述代码中,greeting 参数是可选的。当调用 greet 函数时,我们可以只传递 name 参数,也可以同时传递 namegreeting 参数。

回调函数基础

回调函数是一种作为参数传递给另一个函数,并在该函数内部被调用的函数。它在异步编程、事件处理等场景中广泛应用。例如,JavaScript 中的 setTimeout 函数就接受一个回调函数作为参数:

setTimeout(() => {
    console.log('This is a callback function');
}, 1000);

在这个例子中,箭头函数 () => { console.log('This is a callback function'); } 就是作为回调函数传递给了 setTimeout 函数,它会在 1 秒后被 setTimeout 调用。

可选参数在回调函数定义中的使用

简单回调函数中的可选参数

在定义回调函数类型时,我们同样可以使用可选参数。假设有一个函数,它接受一个数组和一个回调函数,回调函数会对数组中的每个元素进行处理。我们可以这样定义:

type ArrayProcessor = (element: number, index?: number) => number;
function processArray(arr: number[], callback: ArrayProcessor): number[] {
    return arr.map((element, index) => callback(element, index));
}
const result = processArray([1, 2, 3], (element) => element * 2);
console.log(result);

在上述代码中,ArrayProcessor 类型定义了一个回调函数,它接受一个 element 参数,index 参数是可选的。在 processArray 函数中,我们使用 map 方法遍历数组,并调用回调函数 callback。这里我们调用 processArray 时,传递的箭头函数只接受了 element 参数,因为 index 是可选的。

复杂回调函数中的可选参数

当回调函数的逻辑变得复杂时,可选参数能提供更大的灵活性。例如,我们有一个函数用于处理用户输入,并根据不同的配置进行验证。验证函数作为回调函数传递,它可能需要一些额外的配置参数。

type InputValidator = (input: string, config?: { minLength: number; maxLength: number }) => boolean;
function processUserInput(input: string, validator: InputValidator) {
    if (validator(input)) {
        console.log('Input is valid');
    } else {
        console.log('Input is invalid');
    }
}
const simpleValidator: InputValidator = (input) => input.length > 0;
const complexValidator: InputValidator = (input, config) => {
    if (!config) {
        return input.length > 0;
    }
    return input.length >= config.minLength && input.length <= config.maxLength;
};
processUserInput('test', simpleValidator);
processUserInput('test', complexValidator);
processUserInput('test', (input) => input.length < 10);
processUserInput('test', (input, { minLength = 3, maxLength = 10 }) => input.length >= minLength && input.length <= maxLength);

在这个例子中,InputValidator 类型定义的回调函数接受一个 input 参数和一个可选的 config 对象参数。processUserInput 函数使用这个回调函数来验证用户输入。我们定义了 simpleValidatorcomplexValidator 两个验证函数,simpleValidator 忽略了可选的 config 参数,而 complexValidator 根据 config 参数进行更复杂的验证。

可选参数在回调函数调用中的使用

调用时省略可选参数

在调用包含可选参数的回调函数时,我们可以根据实际需求省略可选参数。比如,我们有一个函数用于处理一系列任务,每个任务是一个回调函数,这些回调函数可能需要一些额外的上下文信息,但这个信息是可选的。

type Task = (context?: { data: string }) => void;
function executeTasks(tasks: Task[]) {
    tasks.forEach(task => task());
}
const task1: Task = () => console.log('Task 1 executed');
const task2: Task = (context) => {
    if (context) {
        console.log(`Task 2 executed with context: ${context.data}`);
    } else {
        console.log('Task 2 executed without context');
    }
};
executeTasks([task1, task2]);

在上述代码中,executeTasks 函数调用每个任务回调函数时,都没有传递 context 参数。task1 忽略了这个可能存在的参数,而 task2 对是否有 context 参数进行了判断并做出相应处理。

根据条件传递可选参数

有时候,我们需要根据某些条件来决定是否传递可选参数给回调函数。例如,我们有一个函数用于处理图片加载,回调函数用于在图片加载成功或失败时执行相应操作,并且可以传递一些额外的配置信息给回调函数。

type ImageLoadCallback = (success: boolean, config?: { message: string }) => void;
function loadImage(url: string, callback: ImageLoadCallback) {
    const img = new Image();
    img.onload = () => {
        callback(true, { message: 'Image loaded successfully' });
    };
    img.onerror = () => {
        callback(false);
    };
    img.src = url;
}
loadImage('example.jpg', (success, config) => {
    if (success) {
        if (config) {
            console.log(config.message);
        } else {
            console.log('Image loaded');
        }
    } else {
        console.log('Image load failed');
    }
});

在这个例子中,当图片加载成功时,loadImage 函数传递了 true 和一个包含 message 的配置对象给回调函数;当图片加载失败时,只传递了 false,省略了配置对象。回调函数根据是否接收到配置对象来进行不同的处理。

可选参数在高阶函数回调中的使用

高阶函数定义及回调中的可选参数

高阶函数是指接受一个或多个函数作为参数,或者返回一个函数的函数。在高阶函数的回调函数中使用可选参数可以实现非常灵活的功能。例如,我们有一个高阶函数 compose,它用于组合多个函数,并且组合后的函数的回调可以接受可选参数。

type ComposableFunction = (input: number, extra?: string) => number;
function compose(...funcs: ComposableFunction[]): ComposableFunction {
    return (input, extra) => funcs.reduce((acc, func) => func(acc, extra), input);
}
const addOne: ComposableFunction = (input) => input + 1;
const multiplyByTwo: ComposableFunction = (input, extra) => {
    if (extra === 'double') {
        return input * 2;
    }
    return input * 1;
};
const composedFunction = compose(addOne, multiplyByTwo);
console.log(composedFunction(5));
console.log(composedFunction(5, 'double'));

在上述代码中,compose 函数接受多个 ComposableFunction 类型的函数作为参数,并返回一个新的组合函数。ComposableFunction 类型的函数接受一个 input 参数和一个可选的 extra 参数。addOne 函数忽略了 extra 参数,而 multiplyByTwo 函数根据 extra 参数的值进行不同的计算。

结合泛型的高阶函数回调可选参数

结合泛型,我们可以使高阶函数在回调函数的可选参数使用上更加通用。比如,我们有一个高阶函数 mapAsync,它类似于数组的 map 方法,但处理的是异步操作,并且回调函数可以接受可选参数。

type AsyncMapper<T, U> = (element: T, index?: number, extra?: string) => Promise<U>;
async function mapAsync<T, U>(arr: T[], mapper: AsyncMapper<T, U>): Promise<U[]> {
    const results: U[] = [];
    for (let i = 0; i < arr.length; i++) {
        const result = await mapper(arr[i], i);
        results.push(result);
    }
    return results;
}
const asyncSquare = async (num: number, index, extra) => {
    if (extra === 'triple') {
        return num * num * 3;
    }
    return num * num;
};
mapAsync([1, 2, 3], asyncSquare).then(result => console.log(result));
mapAsync([1, 2, 3], (num, index, extra) => asyncSquare(num, index, 'triple')).then(result => console.log(result));

在这个例子中,mapAsync 是一个高阶函数,它接受一个数组和一个 AsyncMapper 类型的异步映射函数。AsyncMapper 是一个泛型类型,它接受输入类型 T 和输出类型 U,并且回调函数可以接受 element、可选的 index 和可选的 extra 参数。asyncSquare 函数根据 extra 参数的值进行不同的异步计算。

可选参数在事件处理回调中的使用

DOM 事件处理回调中的可选参数

在前端开发中,处理 DOM 事件是常见的操作。TypeScript 中,事件处理回调函数也可以使用可选参数。例如,我们有一个按钮,点击按钮时可能需要传递一些额外的信息给回调函数。

const button = document.createElement('button');
button.textContent = 'Click me';
document.body.appendChild(button);
type ButtonClickCallback = (event: MouseEvent, extraInfo?: { id: number }) => void;
const clickHandler: ButtonClickCallback = (event, extraInfo) => {
    if (extraInfo) {
        console.log(`Button clicked with extra info: ${extraInfo.id}`);
    } else {
        console.log('Button clicked');
    }
};
button.addEventListener('click', (event) => clickHandler(event, { id: 123 }));

在上述代码中,ButtonClickCallback 类型定义了按钮点击回调函数,它接受一个 MouseEvent 参数和一个可选的 extraInfo 对象参数。clickHandler 函数根据是否有 extraInfo 进行不同的处理。

自定义事件处理回调中的可选参数

除了 DOM 事件,我们还可以自定义事件并在其回调函数中使用可选参数。例如,我们创建一个简单的事件发射器类,用于触发自定义事件并传递可选参数给回调函数。

class EventEmitter {
    private events: { [eventName: string]: ((...args: any[]) => void)[] } = {};
    on(eventName: string, callback: (...args: any[]) => void) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }
    emit(eventName: string, ...args: any[]) {
        const callbacks = this.events[eventName];
        if (callbacks) {
            callbacks.forEach(callback => callback(...args));
        }
    }
}
const emitter = new EventEmitter();
type CustomEventCallback = (message: string, extra?: { count: number }) => void;
const customHandler: CustomEventCallback = (message, extra) => {
    if (extra) {
        console.log(`Custom event with extra: ${message}, count: ${extra.count}`);
    } else {
        console.log(`Custom event: ${message}`);
    }
};
emitter.on('customEvent', customHandler);
emitter.emit('customEvent', 'Hello');
emitter.emit('customEvent', 'World', { count: 5 });

在这个例子中,EventEmitter 类用于管理自定义事件。CustomEventCallback 类型定义了自定义事件的回调函数,它接受一个 message 参数和一个可选的 extra 对象参数。customHandler 函数根据是否有 extra 参数进行不同的处理。

可选参数在异步回调函数中的使用

异步操作回调中的可选参数

在异步编程中,回调函数经常用于处理异步操作的结果。例如,我们使用 setTimeout 模拟一个异步操作,并在回调函数中使用可选参数。

type AsyncCallback = (result: string, extra?: { status: 'success' | 'failure' }) => void;
function asyncOperation(callback: AsyncCallback) {
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
            callback('Operation successful', { status: 'success' });
        } else {
            callback('Operation failed', { status: 'failure' });
        }
    }, 1000);
}
asyncOperation((result, extra) => {
    if (extra) {
        console.log(`${result}, status: ${extra.status}`);
    } else {
        console.log(result);
    }
});

在上述代码中,asyncOperation 函数模拟了一个异步操作,它在 1 秒后调用回调函数,并根据操作结果传递不同的 extra 参数。回调函数根据 extra 参数来输出不同的信息。

Promise 回调中的可选参数

当使用 Promise 进行异步操作时,我们也可以在 then 回调函数中使用可选参数。例如,我们有一个函数返回一个 Promise,并且在 then 回调中可以传递可选参数。

type PromiseCallback = (data: number, extra?: { message: string }) => void;
function asyncFunction(): Promise<number> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(42);
        }, 1000);
    });
}
asyncFunction().then((data, extra) => {
    if (extra) {
        console.log(`${data}, ${extra.message}`);
    } else {
        console.log(data);
    }
}, (error) => {
    console.error('Error:', error);
});

在这个例子中,asyncFunction 返回一个 Promise,在 then 回调函数中,我们可以接受 Promise 解析后的值 data 和一个可选的 extra 参数。根据 extra 参数的存在与否,回调函数进行不同的处理。

可选参数在函数重载回调中的使用

函数重载与回调中的可选参数

函数重载允许我们在同一个作用域内定义多个同名函数,但它们的参数列表不同。当涉及到回调函数时,函数重载可以与可选参数结合使用,提供更丰富的功能。例如,我们有一个函数 handleData,它可以接受不同类型的回调函数,这些回调函数可能有可选参数。

type Callback1 = (value: string) => void;
type Callback2 = (value: number, extra: { flag: boolean }) => void;
function handleData(callback: Callback1): void;
function handleData(callback: Callback2): void;
function handleData(callback: (value: string | number, extra?: { flag: boolean }) => void) {
    if (typeof callback === 'function') {
        if (Math.random() > 0.5) {
            callback('Some string');
        } else {
            callback(42, { flag: true });
        }
    }
}
handleData((value) => console.log(`Received string: ${value}`));
handleData((value, extra) => console.log(`Received number: ${value}, flag: ${extra.flag}`));

在上述代码中,我们通过函数重载定义了 handleData 函数的两种形式,分别接受不同类型的回调函数。实际的 handleData 函数实现中,根据随机条件调用回调函数,并根据回调函数的类型传递相应的参数。

结合泛型的函数重载回调可选参数

结合泛型,我们可以让函数重载在回调函数可选参数的使用上更加灵活和通用。例如,我们有一个函数 processItems,它可以处理不同类型的数组,并根据回调函数的类型传递可选参数。

type ItemProcessor<T> = (item: T, index?: number, extra?: { debug: boolean }) => T;
function processItems<T>(arr: T[], callback: ItemProcessor<T>): T[];
function processItems<T>(arr: T[], callback: ItemProcessor<T>) {
    return arr.map((item, index) => {
        const extra = Math.random() > 0.5? { debug: true } : undefined;
        return callback(item, index, extra);
    });
}
const numbers = [1, 2, 3];
const processedNumbers = processItems(numbers, (number, index, extra) => {
    if (extra && extra.debug) {
        console.log(`Processing number at index ${index}`);
    }
    return number * 2;
});
console.log(processedNumbers);
const strings = ['a', 'b', 'c'];
const processedStrings = processItems(strings, (string, index, extra) => {
    if (extra && extra.debug) {
        console.log(`Processing string at index ${index}`);
    }
    return string.toUpperCase();
});
console.log(processedStrings);

在这个例子中,processItems 是一个泛型函数,它接受一个数组和一个 ItemProcessor 类型的回调函数。ItemProcessor 是一个泛型类型,它接受数组元素类型 T,并且回调函数可以接受 item、可选的 index 和可选的 extra 参数。processItems 函数根据随机条件决定是否传递 extra 参数给回调函数。

最佳实践与注意事项

保持回调函数签名的一致性

在使用可选参数的回调函数时,尽量保持回调函数签名的一致性。如果一个回调函数在不同的地方被调用,并且其可选参数的使用方式差异很大,会使代码难以理解和维护。例如,在一个模块中定义的回调函数,在其他模块调用时,应该遵循相同的可选参数使用规则。

// 良好实践
type StandardCallback = (data: any, extra?: { key: string }) => void;
function doSomething(callback: StandardCallback) {
    callback({ value: 123 }, { key: 'test' });
}
function consumeCallback(callback: StandardCallback) {
    callback({ value: 456 });
}
// 不良实践
type InconsistentCallback = (data: any, extra?: { key: string } | number) => void;
function doOtherThing(callback: InconsistentCallback) {
    callback({ value: 789 }, 10);
}
function consumeInconsistentCallback(callback: InconsistentCallback) {
    callback({ value: 111 }, { key: 'another' });
}

在上述代码中,StandardCallback 类型的回调函数保持了可选参数类型的一致性,而 InconsistentCallback 类型的回调函数可选参数类型不一致,这会增加代码理解和维护的难度。

文档化回调函数的可选参数

对于包含可选参数的回调函数,一定要进行充分的文档化。这包括在代码注释中说明每个可选参数的用途、可能的值以及何时需要传递该参数。例如:

/**
 * 处理用户输入的函数,接受一个验证回调函数。
 * @param input 用户输入的字符串
 * @param validator 验证回调函数,接受输入字符串和一个可选的配置对象。
 * 配置对象 `config` 可选参数说明:
 * - `minLength`:输入字符串的最小长度,默认值为 0。
 * - `maxLength`:输入字符串的最大长度,默认无限制。
 */
function processUserInput(input: string, validator: (input: string, config?: { minLength: number; maxLength: number }) => boolean) {
    // 函数实现
}

通过这样详细的文档化,其他开发者在使用这个函数及其回调函数时,能清楚了解可选参数的含义和使用方法。

避免过度使用可选参数

虽然可选参数提供了灵活性,但过度使用可能会使代码逻辑变得复杂。如果一个回调函数有过多的可选参数,可能意味着该回调函数承担了过多的职责,此时可以考虑将其拆分成多个更简单的回调函数。例如:

// 过度使用可选参数
type OvercomplicatedCallback = (data: any, option1?: boolean, option2?: string, option3?: number) => void;
function doComplexThing(callback: OvercomplicatedCallback) {
    // 函数实现
}
// 拆分后的回调函数
type SimpleCallback1 = (data: any, flag: boolean) => void;
type SimpleCallback2 = (data: any, text: string) => void;
type SimpleCallback3 = (data: any, num: number) => void;
function doSimpleThing1(callback: SimpleCallback1) {
    // 函数实现
}
function doSimpleThing2(callback: SimpleCallback2) {
    // 函数实现
}
function doSimpleThing3(callback: SimpleCallback3) {
    // 函数实现
}

在上述代码中,OvercomplicatedCallback 有多个可选参数,使代码逻辑复杂,而拆分后的 SimpleCallback1SimpleCallback2SimpleCallback3 更清晰和易于维护。

类型检查与可选参数

在使用可选参数的回调函数时,要注意 TypeScript 的类型检查。确保在调用回调函数时,传递的参数类型与回调函数定义的类型一致,包括可选参数的类型。例如:

type Callback = (value: string, extra?: { id: number }) => void;
function callCallback(callback: Callback) {
    callback('test', { id: 'not a number' }); // 类型错误,id 应该是 number 类型
}

在这个例子中,传递给 callbackextra 对象中 id 的类型与定义的 number 类型不一致,TypeScript 会报错,提醒开发者修正类型错误。

通过合理使用 TypeScript 可选参数在回调函数中的技巧,我们可以编写更加灵活、可维护的前端代码。从基础概念到各种应用场景,再到最佳实践和注意事项,希望以上内容能帮助你在实际开发中更好地运用这一特性。