Я думаю, что самым простым решением здесь было бы использовать перегрузки:
interface TaskDefinition<T> {
run: () => { skipped: boolean } | { skipped: boolean; result: T };
}
function createTask<T>(task: { run: () => { skipped: boolean; result: T } }): { skipped: boolean; result: T }
function createTask(task: { run: () => { skipped: boolean } }): { skipped: boolean }
function createTask<T>(task: TaskDefinition<T>): { skipped: boolean; result: T } | { skipped: boolean } {
const taskResult = task.run();
if ("result" in taskResult) {
return {
skipped: taskResult.skipped,
result: taskResult.result,
};
}
return {
skipped: taskResult.skipped,
};
}
const inferTest1 = createTask({
run: () => ({
skipped: false,
result: 251,
}),
});
inferTest1.result
const inferTest2 = createTask({
run: () => ({
skipped: false,
}),
});
inferTest2.result // err
Playground Link
Вы также можете использовать условные типы, но я думаю, что это здесь перебор.
Другим решением было бы вывести результат самого запуска, хотя это может помешать другим действиям, выполняемым в функции:
function createTask<T extends { skipped: boolean } | { skipped: boolean; result: T }>(task: { run: () => T }): T {
const taskResult = task.run();
// Only spreading or type assertions will work to satisfy T, also narrowing is now borken
return {
...taskResult
};
}
Playground Link