jeudi 3 janvier 2019

How do I type a decorated property which type is changed by the decorator?

Here's some code that works perfectly in JS:

import Component from '@ember/component';
import {task} from 'ember-concurrency';

class Foo extends Component {
  currentRecordId!: string; // passed from template

  @task
  fetchRecord *(id) {
    return yield this.store.findRecord('foo', id);
  }

  async fetchCurrentRecord() {
    return this.fetchRecord.perform(this.currentRecordId);
  }
}

Ember Concurrency is an alternative to promises that allows cancelling and managing them similar to Observable from RxJS. Since JS promises don't allow cancelling, Ember Concurrency uses yield instead of async/await.

The task decorator used above converts a generator function into a TaskProperty instance that has a .perform() method.

Please note, that, though weird, this pattern has proven its handiness and reliability in non-typed JS apps.

But typing it presents a challenge.


Here are

export declare function task<T, A>(generatorFn: () => Iterator<T>): Task<T, () => TaskInstance<T>>;

export declare function task<T, A>(
  generatorFn: (a: A) => Iterator<T>
): Task<T, (a: A) => TaskInstance<T>>;

export declare function task<T, A>(
  generatorFn: (a: A) => Iterator<PromiseLike<T>>
): Task<T, (a: A) => TaskInstance<T>>;

export declare function task<T, A1, A2>(
  generatorFn: (a1: A1, a2: A2) => Iterator<T>
): Task<T, (a1: A1, a2: A2) => TaskInstance<T>>;

// More variants of arguments skipped

export interface TaskInstance<T> extends PromiseLike<T> {
  readonly error?: any;
  readonly hasStarted: ComputedProperty<boolean>;
  readonly isCanceled: ComputedProperty<boolean>;
  readonly isDropped: ComputedProperty<boolean>;
  readonly isError: boolean;
  readonly isFinished: ComputedProperty<boolean>;
  readonly isRunning: ComputedProperty<boolean>;
  readonly isSuccessful: boolean;
  readonly state: ComputedProperty<TaskInstanceState>;
  readonly value?: T;
  cancel(): void;
  catch(): RSVP.Promise<any>;
  finally(): RSVP.Promise<any>;
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?: ((value: T) => TResult1 | RSVP.Promise<TResult1>) | undefined | null,
    onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
  ): RSVP.Promise<TResult1 | TResult2>;
}

interface Task<T> extends TaskProperty<T> {
    readonly isIdle: boolean;
    readonly isQueued: boolean;
    readonly isRunning: boolean;
    readonly last?: TaskInstance<T>;
    readonly lastCanceled?: TaskInstance<T>;
    readonly lastComplete?: TaskInstance<T>;
    readonly lastErrored?: TaskInstance<T>;
    readonly lastIncomplete?: TaskInstance<T>;
    readonly lastPerformed?: TaskInstance<T>;
    readonly lastRunning?: TaskInstance<T>;
    readonly lastSuccessful?: TaskInstance<T>;
    readonly performCount: number;
    readonly state: TaskState;
    perform(...args: any[]): TaskInstance<T>;
    cancelAll(): void;
}

export interface TaskProperty<T> extends ComputedProperty<T> {
    cancelOn(eventNames: string[]): this;
    debug(): this;
    drop(): this;
    enqueue(): this;
    group(groupPath: string): this;
    keepLatest(): this;
    maxConcurrency(n: number): this;
    on(eventNames: string[]): this;
    restartable(): this;
}

These types aren't official and can be customized.


I struggle with properly typing the topmost code sample.

The error I'm getting is:

Property perform does not exist on type () => IterableIterator<any>.

It is understandable, since fetchRecord is defined as a generator.

Moreover, TypeScript officially does not support decorators that change the type of decorated property.

So the question is: how to work around the limitation and type such a decorator without reverting to @ts-ignore?

In addition to typing the fetchRecord property, I would like to properly type the arguments that I pass into this.fetchRecord.perform() and which are received by the generator.

Thank you. ^__^




Aucun commentaire:

Enregistrer un commentaire