facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.1k stars 1.86k forks source link

My usecase #5297

Open MOZGIII opened 7 years ago

MOZGIII commented 7 years ago

Hello!

Here's my usecase for flow, and I need help with it. I have a code like below, and I want to add poper types for functions that are exptored from this module (and for helpers object).

The code is:

// @flow
import * as utils from "./utils";
import toCamelCase from "./toCamelCase";

const map = {
  root: "/",

  login: "/login",
  logout: "/logout",

  workspace: "/workspaces/:workspace_id",
  import: "/workspaces/:workspace_id/import",
  assets: "/workspaces/:workspace_id/assets",
  asset: "/workspaces/:workspace_id/assets/:id",
  preferences: "/workspaces/:workspace_id/user/preferences",
  files: "/workspaces/:workspace_id/files"
};

export type RouteName = $Keys<typeof map>;

// buildHelpers provides a useful helpers that are intended to
// be used instead of the direct routes invocation.
const buildHelpers = (routeName: RouteName) => ({
  [toCamelCase(`${routeName}_pattern`)]: utils.routeByName(routeName),
  [toCamelCase(`${routeName}_path`)]: (params: *) =>
    utils.generatePath(routeName, params),
  [toCamelCase(`match_${routeName}_path`)]: (path: string) =>
    utils.matchPath(routeName, path),
  [toCamelCase(`match_${routeName}_sub_path`)]: (path: string) =>
    utils.matchSubPath(routeName, path)
});

const helpers = {};

Object.keys(map).forEach(key => {
  const newHelpers = buildHelpers(key);
  Object.assign(helpers, newHelpers);
});

export default helpers;

Expected types would look like this (for each entry in map):

workspacePattern: "/workspaces/:workspace_id" // or just string
workspacePath: (params: *) => string
matchWorkspacePath: (path: string) => MatchType
matchWorkspaceSubPath: (path: string) => MatchType

How can I achieve this?

MOZGIII commented 7 years ago

FYI: I've just changed the way code is organized in my project as a workaround. But this particular sample is interesting just as a usecase example.

bradennapier commented 7 years ago

@MOZGIII from what I can tell the design here may flawed in a few ways. I couldn't tell completely what you were trying to accomplish, but it did seem needlessly complex (making it hard to type as well).

If you want a normalized object and you want to transform keys etc then flow isn't going to be able to type it very well. It can't take a string literal and generate a new string literal from it (hello => hello_world), so you would need to change to using string typing only which probably is not ideal.

An alternative would be to instead use Flow to describe a transformation and use a layered object.

If you were to do that then you would be able to type this much more naturally and create code which is far easier to read.

Note: I had a pretty hard time understanding specifically what you were trying to accomplish here and didn't feel like taking the time to really break it down. This seems to do something along the lines of what you were tyring to do.

I didn't know what MatchType was supposed to be so I changed that to any and * was changed to mixed

The below should also provide 100% coverage which would have been difficult with the first example.

// @flow
import * as utils from './utils';
import toCamelCase from './toCamelCase';

const map = {
  root: '/',

  login: '/login',
  logout: '/logout',

  workspace: '/workspaces/:workspace_id',
  import: '/workspaces/:workspace_id/import',
  assets: '/workspaces/:workspace_id/assets',
  asset: '/workspaces/:workspace_id/assets/:id',
  preferences: '/workspaces/:workspace_id/user/preferences',
  files: '/workspaces/:workspace_id/files',
};

type MatchType = any;

export type RouteNames = $Keys<typeof map>;

type RouteMap<T> = {|
  +pattern: $ElementType<typeof map, T>,
  +path: (params: mixed) => string,
  +matchPath: (path: string) => MatchType,
  +matchSub: (path: string) => MatchType,
|};

type TransformRoutesMap = <T>(T) => RouteMap<T>;

export type RoutesMapped = $ObjMapi<typeof map, TransformRoutesMap>;

const helpers: $Exact<RoutesMapped> = Object.keys(map).reduce(
  (p: $Shape<RoutesMapped>, key): $Shape<RoutesMapped> => ({
    ...p,
    [key]: {
      pattern: map[key],
      path: (params: mixed): string => utils.generatePath(key, params),
      matchPath: (path: string): MatchType => utils.matchPath(key, path),
      matchSub: (path: string): MatchType => utils.matchSubPath(key, path),
    },
  }),
  ({}: $Shape<RoutesMapped>),
);

const workspace = helpers.workspace;

// $Works
(workspace.pattern: '/workspaces/:workspace_id');

// $Works
(workspace: RouteMap<'workspace'>);

// $ExpectError
(workspace: RouteMap<'assets'>);

export default helpers;

Essentially it ends up taking your map object and creating a type where each key becomes an object that matches RouteMap where its value (pattern) is added to a key (pattern) and the rest follows.

Example:

const mapped = {
  workspace: {
    pattern: '/workspaces/:workspace_id',
    path: (params: mixed): string => utils.generatePath('workspace', params),
    matchPath: (path: string): MatchType => utils.matchPath('workspace', path),
    matchSub: (path: string): MatchType =>
      utils.matchSubPath('workspace', path),
  },
  // ...rest here
};

Finally here is a screenshot to help see what the $ObjMapi does here:

image