How to declare external interfaces with FlowType

  • 6
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

In between seeing the sights in Nagoya, doing some RxJSNext stuff, and eating konbini food, I've been trying to play around with FlowType. It's a gradual typing tool that works fairly well, except for some bits that aren't really documented that well or are known issues that aren't really documented a lot (i.e. is common knowledge amongst people at FB and long-term FlowType users, but surprising to others). It's pretty nice overall though, and I encourage you to try it out.

The problem

While Flow is pretty good at figuring out what is going on in your code even if you give it minimal type annotations, it doesn't really know how to figure out what all the code in the known universe (i.e. your node_modules) is going to. The guide that is on the FlowType docs here continues on to here and explains the gist of it pretty well, but leaves out a big part: Flow will prefer the original files in your node_modules over your typedefs, so you need to ignore the original files in your .flowconfig to get this to work.

Setup

I'm working off of my original redux-rx-fun project, so let's go over what all's in my .flowconfig for that project:

[ignore]
build <-- ignore the build output of my project
.*/node_modules/redux/.* <-- don't try to infer from redux sources
.*/node_modules/rx/.* <-- don't try to infer from rx sources
.*/node_modules/react/.* <-- don't try to infer from react sources
.*/node_modules/fbjs.* <-- don't try to infer from fbjs sources
.*/node_modules/babel.* <-- don't try to infer from babel sources
.*/node_modules/babylon.* <-- don't try to infer from babylon sources

[include]

[libs]
interfaces/ <-- DO use the definitions from my declared interfaces

[options]
esproposal.class_static_fields=enable <-- enable static class properties (they aren't ES6)

Why ignore all of these? Well...

  • We don't need to check a whole lot of Webpack/Babel generated code.
  • We are going to define Redux and Rx interfaces ourselves.
  • Flow ships with React definitions, so you need to ignore the sources.
  • FBJS is broken, so it makes Flow unhappy. Don't worry, we don't need to check it really.
  • Babel also is not Flow-happy, but we don't really care.
  • Babylon is also not Flow-happy, but we don't really care.

So now that we have our setup, let's go take a look at our definitions.

Definitions

Redux

Let's start with the Redux definitions, since these are simple enough.

// Finally, declare the module 'redux' that will be imported in our files
declare module 'redux' {
  // declare the "Reducer" type for Redux, a function of state and action.
  declare type Reducer<T, R> = (state: T, action: R) => T;

  // declare the type of the "Store" in Redux (as far as we care, that is).
  declare interface Store<T, R> {
    subscribe(observer: () => void): void;
    dispatch(action: R): void;
    getState(): T;
  }

  // createStore takes our Reducer and produces a Store. Simple as that.
  declare function createStore<T, R>(reducer: Reducer<T, R>): Store<T, R>;
}

You can go look at the usages and see that while they don't explicitly have annotations everywhere, they match and Flow will catch errors (e.g. here)

Rx

Then we have our Rx definitions, which are more complex, but really nothing special.

declare module 'rx' {
  // declare an Observable that boxes values of type T
  declare class Observable<T> {
    // merge can really take any type and merge them together, but I figured it's easiest
    // to handle it this way.
    static merge<R>(...sources: Observable<R>[]): Observable<R>;

    // similar story with map -- map can project in other types (probably what you want usually).
    map<R>(f: (item: T) => R): Observable<R>;

    // scan will take the values of type T, sure,
    // but it will accumulate something of a different type usually.
    scan<R>(f: (prev: R, next: T) => R): Observable<R>;

    // You might prefer startWith with type T, but I often use startWith to provide
    // an event for my Observables using scan, which also will be used on subsequent
    // streams as the "prev" value I defined above.
    // This means my Observable is effectively [R, ...T[]], sure, which means that
    // really my Observable is of type Observable<R|T>, but this is fine with me.
    // More strict people might be offended.
    startWith<R>(init: R): Observable<T>;

    // subscribe can take optional arguments, but I'm going to go ahead and
    // require the "next" observer, at the very least.
    subscribe(
      next: (item: T) => any,
      error?: (error: any) => any,
      complete?: (item: T) => any
    ): {
      // this is really a IDisposable in Rx, but I'm kind of lazy and don't plan on using much of it here.
      unsubscribe: () => void;
    };
  }

  // good old Subject, to which you can insert items for events.
  declare class Subject<T> extends Observable<T> {
    onNext(item: T): void;
  }
}

Likewise, Flow will check our usages of these types (e.g. here).

Conclusion

So FlowType is really nice and works pretty well, but there are some warts with the way you have to configure it right now. Some of these issues might get fixed in future versions, but for now, this seems to be what all you have to do to get typing working with external dependencies you bring in.

Hopefully this comes up the next time people are googling around trying to figure out their issues with FlowType. At the very least, it'll help shave a couple hours off of trying to read through a bunch of issues or code and trying to ask people what is even going on.

Links