Условные типы машинописного текста, выведенные функцией высокого порядка

У меня есть функция, которая может возвращать синхронный или асинхронный результат.

type HookHandler<T> = (context: MyClass<T>) => boolean | Promise<boolean>;

и класс, который принимает список этих функций

class MyClass<T> {

    constructor(private handlers: Array<HookHandler<T>>) {

    }

    public invokeHandlers() : boolean | Promise<boolean> {
        // invoke each handler and return:
        // - Promise<boolean> if exist a handler that return a Promise<T>
        // - boolean if all handlers are synchronous
    }

}

Мне было интересно, есть ли шанс заставить машинописный текст вывести тип возвращаемого значения invokeHandlers() на основе заданных обработчиков. Учтите, что все обработчики объявлены во время разработки:

const myClassSync = new MyClass<MyType>([
   (ctx) => true,
   (ctx) => false
]);

const myClassAsync = new MyClass<MyType>([
   async (ctx) => Promise.resolve(true),
   async (ctx) => Promise.reject()
]);

const myClassMix = new MyClass<MyType>([
   async (ctx) => Promise.resolve(true),
  (ctx) => true
]);

Могу ли я сделать возвращаемый тип invokeHandlers() зависимым от типов текущих заданных обработчиков без явного приведения? Так например

// all handlers are sync, infer boolean
const allHandlersAreOk: boolean = myClassSync.invokeHandlers()

// all handlers are async, infer Promise<boolean>
const allAsyncHandlersAreOk: Promise<boolean> = await myClassAsync.invokeHandlers()

// at least one handler is async, infer Promise<boolean>
const allMixedHandlersAreOk: Promise<boolean> = await myClassMix.invokeHandlers()

Очевидно, я могу вернуть простой Promise<boolean>, но я бы потерял возможность вызывать invokeHandlers() в синхронном контексте, а этого хотелось бы избежать.

Любые предложения или другой выбор дизайна для решения проблемы? Спасибо!

Возможно, у меня есть решение для вас, но ваш код не имеет структурной зависимости от параметра T, поэтому я планирую его исключить.

jcalz 28.02.2019 15:26

Общий тип скопирован из реального кода, это просто упрощение.

Giacomo De Liberali 28.02.2019 15:33
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
2
363
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

То, что некоторые из них могут возвращать обещания, является фактом. Это самый известный TypeScript может.

Являются ли они возвращаемыми промисами или нет, можно определить только во время выполнения.

Итак, ответ — нет, TypeScript не может вывести что-то, что можно вывести только во время выполнения.

Имеет смысл. Может быть для вас решением разделить invokeHandlers() на две функции, одна из которых возвращает boolean, а другая возвращает Promise<boolean>?

Giacomo De Liberali 28.02.2019 15:20

Да, но тогда тот, который может возвращать только boolean, должен быть набран таким образом, чтобы его параметры никогда не были промисами, что вынуждает вас (как программиста) использовать правильный с самого начала. И это в основном устранило бы преимущество возможности смешивать асинхронные и синхронизирующие обработчики. ИЛИ вы можете сделать проверку во время выполнения, а не во время компиляции, но тогда вы потеряете преимущества TypeScript.

Sergiu Paraschiv 28.02.2019 15:25

Да, в этом была суть. В настоящее время я игнорирую асинхронные обработчики, если вызывающая сторона использует синхронизированную версию invokeHandlers(), но я искал более чистое решение.

Giacomo De Liberali 28.02.2019 15:30

вы можете использовать перегрузки, если у вас есть способ различать ваши обработчики или каким-то образом идентифицировать их во время выполнения.

function handler(x: number): string;
function handler(y: string): number;
function handler(arg) {
    if (typeof arg === 'number') {
        return `${arg}`
    } else {
        return parseInt(arg);
    }
}

const inferred = handler(1); // <-- typescript correctly infers string
const alsoInferred = handler('1'); // <-- typescript correctly infers number

Итак, если бы вы могли написать что-то вроде:

function handler(context: AsyncHandler): Promise<boolean>;
function handler(context: MixedHandlers): Promise<boolean>;
function handler(context: SyncHandlers): boolean:
function handler(context){
  // your implementation, maybe instanceof if each type has a class representation
}

TypeScript может правильно определить возвращаемый тип. Я не уверен, возможно ли это на основе вашей структуры кода, но я решил поделиться. Подробнее здесь, особенно раздел о перегрузках

Спасибо, я попробую

Giacomo De Liberali 28.02.2019 15:39
Ответ принят как подходящий

Вот как я бы подошел к этому:

Придумайте отдельные типы для каждого возможного обработчика хука:

type SyncHookHandler = (context: MyClass<any>) => boolean;
type AsyncHookHandler = (context: MyClass<any>) => Promise<boolean>;
type HookHandler = AsyncHookHandler | SyncHookHandler;

А затем сделайте MyClass зависимым от типа HHHookHandler, который вы используете. Тип возвращаемого значения invokeHandlers может быть условный тип, который оценивается как boolean, если HH равно SyncHookHandler, и Promise<boolean>, если HH равно AsyncHookHandler или AsyncHookHandler | SyncHookHandler:

class MyClass<HH extends HookHandler> {

  constructor(private handlers: Array<HH>) { }

  public invokeHandlers(): Promise<boolean> extends ReturnType<HH> ? 
    Promise<boolean> : boolean;
  public invokeHandlers(): boolean | Promise<boolean> {

    const rets = this.handlers.map(h => h(this));

    const firstPromise = rets.find(r => typeof r !== 'boolean');
    if (firstPromise) {
      return firstPromise; // ?‍ what do you want to return here
    }
    // must be all booleans
    const allBooleanRets = rets as boolean[];
    return allBooleanRets.every(b => b);  // ?‍ what do you want to return here 
  }
}

Я только что сделал глупую реализацию внутри invokeHandlers(), чтобы дать представление о том, что вы будете там делать. Теперь вы можете видеть, что ваш код ведет себя так, как ожидалось.

const myClassSync = new MyClass([
  (ctx) => true,
  (ctx) => false
]);
// all handlers are sync, infer boolean
const allHandlersAreOk: boolean = myClassSync.invokeHandlers()

const myClassAsync = new MyClass([
  async (ctx) => Promise.resolve(true),
  async (ctx) => Promise.reject()
]);
// all handlers are async, infer Promise<boolean>
// note you do not "await" it, since you want a Promise
const allAsyncHandlersAreOk: Promise<boolean> = myClassAsync.invokeHandlers()

const myClassMix = new MyClass([
  async (ctx) => Promise.resolve(true),
  (ctx) => true
]);
// at least one handler is async, infer Promise<boolean>
// note you do not "await" it, since you want a Promise
const allMixedHandlersAreOk: Promise<boolean> = myClassMix.invokeHandlers()

Это работает для вас?

Обратите внимание, что поскольку код примера не имел структурной зависимости от универсального параметра T, у меня удалил это. Если вам это нужно, вы можете добавить его обратно в соответствующие места, но я предполагаю, что вопрос больше касается обнаружения синхронизации, если вы можете, а не какого-то общего типа.

Хорошо, надеюсь, это поможет; удачи!

Другие вопросы по теме