flarum / issue-archive

0 stars 0 forks source link

Various typing issues #96

Open davwheat opened 3 years ago

davwheat commented 3 years ago

Copied from @clarkwinkelmann's comment on flarum/framework#2343.


Here's a list of things I noticed related to the types package. Some of them are noticeable just by using the types package, other only start causing issues when manually enabling typescript on the extension.

I have not checked if any has been already fixed in dev-master. Those issues were present in beta 16:

The case of Model static methods

The method incorrectly type-hint the output of the method when it should type-hint the output of the anonymous methods returned by those methods.

This causes a call like myModel.user() to throw an error "Cannot invoke an object which is possibly undefined" because it thinks myModel.user might be undefined and not a method, when it's actually the result of () which might be undefined.

I also think those methods should support a generic so extensions can define the return type of the anonymous function. This is what I used as a workaround for now:

export default class Model extends OriginalModel {
    static attribute<T = any>(name: string, transform?: Function): (value?: string) => T | undefined {
        return OriginalModel.attribute(name, transform);
    }

    static hasOne<T extends OriginalModel = OriginalModel>(name: string): () => T | false | undefined {
        return OriginalModel.hasOne(name) as any;
    }

    static hasMany<T extends OriginalModel = OriginalModel>(name: string): () => T[] | false {
        return OriginalModel.hasMany(name);
    }
}

Loaded/oninit attributes

One problem I have faced in multiple components are temporarily nullable attribute.

I often have variables that are only initialized in oninit, but if I just type-hint them as their final values in the class, typescript complains I didn't allow for the undefined value they will have between the constructor and oninit. I have found !: to be a solution to these, like

class MyComponent extends Component {
    tag!: Tag;

    oninit (vnode) {
        super.oninit(vnode);

        this.tag = this.attrs.tag;
    }
}

However I have not yet found a good solution for attribute that are null until the first render, like this:

class MyComponent extends Component {
    tag: Tag | null = null;

    oninit (vnode) {
        super.oninit(vnode);

        // Simulate network request
        setTimeout(() => {
            this.tag = this.attrs.tag;
            m.redraw();
        }, 1000);
    }

    view () {
        if (!this.tag) {
            return LoadingIndicator.component();
        }

        return Button.component({
            onclick: () => {
                // Typescript will complain that this.tag could be null, because this runs inside of a callback
                // and the value might be null again. However we know the value will never get back to null at this point.
                alert(this.tag.name());
            },
        }, this.tag.name()); // This is fine, because typescript sees the check for null above in the same function
    }
}

I'm not yet sure how to write this without creating a second component that takes the variable as an attribute from the same component and make it non-nullable.

Originally posted by @clarkwinkelmann in https://github.com/flarum/core/issues/2343#issuecomment-826394029

clarkwinkelmann commented 3 years ago

Edited by davwheat: these have been added to the OP task list

A few more:

Another thing that requires consideration is how to type-hint Store.pushPayload(). I manually typed the output as MyModel[] but that doesn't cover the .payload property that gets added to the array. This method will probably need a <T> property that then translates to an interface that's the T[] & the payload: any property.

clarkwinkelmann commented 3 years ago

Other thing: we need a type-hint for app under common namespace. In 1.0+ there's no way to import app there. flarum/app has no typings, and importing flarum/admin/app causes error in forum and vice-versa.

davwheat commented 3 years ago

In regards to the app typings, I think we should remove them from the global typings (which should be fixed to actually work in extensions soon), and instead enforce importing from flarum/xxx/app.

If we don't we'd need to do something like:

declare const app: import('../src/common/app') | import('../src/forum/app') | import('../src/admin/app')

...which doesn't seem to be liked by TS (causes about 400+ typing errors).

clarkwinkelmann commented 3 years ago

I used & between the app imports when I made by local types declarations for this. Of course it has the downside of allowing use of methods that are not actually available in the current frontend.

As long as there's an app in all namespaces including common, I think it's fine if we drop them, at least from the typings.

The global app object is useful for tests using the browser console, but in the code itself it shouldn't have a big impact to actually import it, since that'll be compressed anyway.