Skip to content

Latest commit

 

History

History

styleguide

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

Runtastic Flow Style Guide

Table of Contents

  1. Preface
  2. Type Names
  3. Type Definitions
  4. Function Headers
  5. Type Inference
  6. Libdef Files

Preface

This document discusses our ways of writing flowtyped JavaScript source code for Runtastic related projects.

To stay consistent between examples, it is assumed that source code is located in [PROJECT]/src/ and its transpiled output in [PROJECT]/lib/. Paths may vary project-wise, but we try to stick to this convention as best as possible.

⬆ back to top

Type Names

Type names are a very important part of documentation, so it must be clear what intentions each type has, e.g. a consumer of a type should intuitively know what meta-type (Object, Function or primitives like string, number,...) they represent. Also names must not be ambiguous to prevent mistaking JavaScript with Flow code.

  • 1.1 Capitalize & CamelCase type names

Why? This will help us prevent collisions with variable names etc.

// bad
type foo = string;
type myFoo = string;
type JSONApiEntity = {};

// good
type Foo = string;
type MyFoo = string;
type JsonApiEntity = {};

  • 1.2 Functions: Add a Fn / Cb suffix to mark types as functions / callbacks

Why? We realized that this naming convention was especially beneficial for our functional APIs, which utilize a lot of function composition / currying / mapping etc.

// bad
type Http = () => Promise<Object>;

// good
type HttpFn = () => Promise<Object>;
type DoProfilePressFn = (profile: Object) => void;

// also okay
type OnProfilePressedCb = (profile: Object) => void;

⬆ back to top

Type Definitions

When handling a complex code-base, you will find yourself in the situation where you have to wrangle a lot with type aliases & definitions, which are eventually dependent on each other (and maybe spread over different files). To make things easier to follow, we need some rough guidelines when and where it makes sense to expose those types.

To set the stage, we are talking about type definitions like this:

// Publicy exposed by the (sub)module
export type MyType = 'value1' | 'value2';

// Defined in the module scoped only
type LocalType = Object;

// Export forwarding
export type * from 'someTypes';

  • 2.1 Dedicated Type Files: Gather common types into Type files / submodules. Rule of thumb: Use a dedicated type file as soon as a type represents an integral entity of the whole module.

    Why? In many cases this reduces mental overhead whenever types of a specific category are used in different parts of the application / library. Also those files make a great target for library type exports (so the types can be used in other modules as well).

    // ======================
    // Example 1:
    // Here we put all unit-related Types in the src/units.js and import
    // from the same file within e.g. our formatters
    // ======================
    
    // src/units.js
    export type DistanceUnit = 'km'
      | 'mi'
      | 'm'
      | 'cm';
    
    export WeightUnit = 'kg' | 'lbs' | 'st';
    
    // src/format/formatWeight.js
    import type { WeightUnit } from '../units';
    
    export function formatWeight(value: number, unit: WeightUnit): string {
      //...
    }
    
    // ======================
    // Example 2:
    // For more complex collections of types, it is also totally viable to create whole
    // type submodule structures with index forwarding:
    // ======================
    
    // src/types/units.js
    export SomeUnit = 'x';
    
    // src/types/index.js
    export type * from 'units';
    
    // src/someConsumer.js
    import { SomeUnit } from './types';  

  • 2.2 (Sub)module Related Types: Export types which are purely related to one specific submodule / function from the same file as the implementation.

    Why? In the contrary to the common type file, sometimes a type definition only makes sense in a very specific context of a specific module / function. This is especially true for function modules, which expose the function interface as a type as well (for functional concepts etc.).

    Most of the time, these types are mostly so specific that you don't really need to explicitly import / use them... Flow usually does a good job in inferring those types anyways.

    // ======================
    // Especially for these occasional util-modules, it is very common
    // to export its types directly from the same file
    // ======================
    
    // src/util/jsonapi.js
    export type JsonApiResponse = {
      data?: Object,
      links?: Object,
      meta?: Object,
      included?: Array<Object>,
    };
    
    export Entity = {
      data?: Object,
      links?: Object,
      meta?: Object,
    };
    
    export function denormalize(jsonApiResponse: JsonApiResponse): Entity {
      // ...
    }
    
    // src/someModule.js
    import type { Entity } from './util/jsonapi'; // Entity only makes sense in the jsonapi context

  • 2.3 Local vs Export Types: Do not unnecessarily export types if they are not useful for other modules. Try to be consistent with names inside a module's (say: "npm package's") scope

    Why? Local Types are a good way to express a specific context and we are not forced to import types from other files. Sometimes it is easier to just rewrite types instead of centralizing them (less file dependencies).

    // src/some/module.js
    
    // More generic names are totally okay as long as they are scoped to the module
    type Options = { test: string };
    
    export function foo(options: Options): string {
      return 'bar';
    }

  • 2.4 Prevent Name Collisions: Do not export multiple types from different submodules with the same name inside a module's scope.

    Why? This will prevent name collisions whenever we try to do an export type * from './mytypes'; and also prevent confusion on the module level.

    Bad:

    // src/format1.js
    export type FormatFn = (arg: Object) => string;
    
    // src/format2.js
    export type FormatFn = (arg: number) => string;
    
    // src/index.js
    export type * from './format1';
    export type * from './format2';
    
    // test.js
    import type { FormatFn } from './src'; // ??? What type is FormatFn ???

    Good:

    // src/format1.js
    export type FormatObjectFn = (arg: Object) => string;
    
    // src/format2.js
    export type FormatNumberFn = (arg: number) => string;
    
    // src/index.js
    export type * from './format1';
    export type * from './format2';
    
    // test.js
    import type { FormatObjectFn } from './src'; // Nice!

  • 2.5 The typeof Operator: Use the typeof operator to reflect type information of a specific function / class. (Details: https://flowtype.org/docs/typeof.html)

    Why? Before we learned about the existence of the typeof operator, we defined concrete function definitions as types by duplicating the interface, which complicates future maintanence

    Bad:

    // src/convert.js
    type ConvertFn = (to: Unit, unitValue: ?UnitValue) => ?UnitValue;
    function convert(to: Unit, unitValue: ?UnitValue): ?UnitValue { /* ... */ }

    Good:

    // src/convert.js
    type ConvertFn = typeof convert; // Much more DRY approach
    function convert(arg: HttpArg): Promise<HttpResult> { /* ... */ }
    
    // it's also viable to do typeof operations on the fly!
    import { convert } from './src/convert';
    
    function higherOrderStuff(convertFn: typeof convert) { /* ... */ }

⬆ back to top

Function Headers

In many cases you will end up with some very complex function parameter lists or return types, which are getting messy to read after the addition of flow-types, if you don't stick to a guideline.

Here we will give some hints how to format these annotations appropriately.

  • 3.1 Complex Arguments: Try not to inline Function or Object definitions in function headers (especially for export functions).

    Why? Because inlined Function / Object definitions are subjectively hard to read and most of the time explicitly defined types are easier to maintain.

    Bad:

    // Inline types makes it kinda hard to read, mkay?
    function something(option: { a: string, b: string }, cb: (a: Object) => string): Promise<string> {
      // Function body
    }

    Good:

    // Explicit types makes it a little bit easier on the eyes
    type Option = {
      a: string,
      b: string,
    };
    
    type CallbackFn = (a: Object) => string;
    
    function something(option: Option, cb: CallbackFn): Promise<string> {
      // Function body
    }

  • 3.2 Complex Return Values: Don't inline complex Object / Function types as return types (excluding generic primitives / interfaces).

    Why? Mainly for better readablity.

    // bad
    function createObject(a: string, b: string): { a: string, b: Function } { /* ... */ }
    
    // good
    type SomeObj = { a: string, b: string };
    function createObject(a: string, b: string): SomeObj { /* ... */ }
    
    // also good
    function something(): Promise<string> { /* ... */ }

  • 3.3 Long Argument Lists: If the function definition reaches the line max-len, put arguments in separate lines.

    // bad
    function something(arg1: string, arg2: string, arg3: string, arg4: string, arg5: string): string {
      /* ... */
    }
    
    // good
    function something(
      arg1: string,
      arg2: string,
      arg3: string,
      arg4: string,
      arg5: string,
      ): string {
        /* ... */
    }
    
    // also good with certain limit
    function something(arg1: string, arg2: number, arg3: Object): string {
      /* ... */
    }

⬆ back to top

Type Inference

  • 4.1 Exported Interfaces: Always fully annotate interfaces which are exposed via the export keyword

    Why? Flow will report exported values without annotations as an error to prevent undocumented public interfaces.

    // bad
    export function foo(arg) {
      // Usually, flow could infer the types, but since it is exported,
      // it will report an error
      return `${arg} - foo`;
    }
    
    // good
    export function foo(arg: string): string {
      return `${arg} - foo`; // This will work
    }

  • 4.2 Non-Exported Interfaces: Utilize the inference system for arrow functions, function body code as much as possible.

    Why? Flow does a really good job inferring (guessing) types. You don't have to worry about uncovered edge-cases. If flow cannot infer a type, it will report an error and will ask for more annotations.

    Bad:

    const list: Array<string> = ['foo', 'bar']; // unnecessarily verbose
    
    // Don't do this annotation madness... Flow can guess this super easily
    list.reduce((result: string, next: string): string => `${result},${next}`, '');

    Good:

    const list = ['foo', 'bar'];
    
    list.reduce((result, next) => `${result},${next}`, '');

⬆ back to top

Libdef Files

5.1 Using and Vendoring js.flow Files ("Shadow Files")

Flow offers different methods to infer types for third-party source code. One of the easiest ways to vendor flow-type declarations is by copying the original source of each vendored file and put it in an accompanying file with a .js.flow suffix.

Ideally after each build, files should end up in the project similar to this:

|- src/
   |- sub1
      |- file1.js
|- lib/
   |- sub1
      |- file1.js
      |- file1.js.flow
|- ...

For now, we use a bash script to automatically create those .js.flow definitions. We will eventually work on a platform independent solution (node?) when our requirements change.

For our projects, we always vendor complementary flow.js files. Unfortunately, we don't have a solution for minified source code yet (except for writing libdef declarations by hand).

Also, this method will eventually cause problems with future major (breaking) flow updates. Until the community made up their mind about how to handle these dependencies, we will hopefully be able to update flow-bin dependencies in consuming codebases as soon as we do in our libraries.

5.2 External Libdef Files

In many occassions, it is not viable to follow the js.flow approach, many times you will find yourself in following scenarios:

  • Modules are not typed with flow
  • Modules are consumed as single min.js file (UMD, AMD,..)
  • Modules / Tools require a specific global environment (e.g. mocha)
  • Modules utilize a breaking newer version of flow

Since we try to achieve a 100% flow coverage, there needs to be a balance between value gain of using another node-module and type-safetiness.

For maintaining and retrieving libdef files of more popular projects we use flow-typed. By default, the flow-typed cli binary will install downloaded libdef files in [PROJECT]/flow-typed/npm, which are tagged by flow & module version. These files always have to be tracked in git.

Whenever you use flow-typed, make sure to add the path flow-typed/ in the [LIB] section of your project's .flowconfig. Otherwise, flow might not pick up the libdef files.

If you have to write a libdef on your own, make sure to put these definitions directly in the flow-typed, but not in the flow-typed/npm subdirectory.

⬆ back to top