{"id":13850405,"url":"https://github.com/AveroLLC/types-first-ui","last_synced_at":"2025-07-12T22:30:28.894Z","repository":{"id":97558880,"uuid":"136230959","full_name":"AveroLLC/types-first-ui","owner":"AveroLLC","description":"An opinionated framework for building long-lived, maintainable UI codebases","archived":false,"fork":false,"pushed_at":"2018-11-27T18:06:51.000Z","size":934,"stargazers_count":114,"open_issues_count":3,"forks_count":4,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-08-05T20:32:37.912Z","etag":null,"topics":["react","redux","redux-observable","rxjs","typesafe","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AveroLLC.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-06-05T20:26:40.000Z","updated_at":"2023-03-28T21:23:59.000Z","dependencies_parsed_at":"2024-01-13T17:22:28.381Z","dependency_job_id":null,"html_url":"https://github.com/AveroLLC/types-first-ui","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AveroLLC%2Ftypes-first-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AveroLLC%2Ftypes-first-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AveroLLC%2Ftypes-first-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AveroLLC%2Ftypes-first-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AveroLLC","download_url":"https://codeload.github.com/AveroLLC/types-first-ui/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225839486,"owners_count":17532305,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","redux","redux-observable","rxjs","typesafe","typescript"],"created_at":"2024-08-04T20:01:11.100Z","updated_at":"2024-11-22T03:31:23.124Z","avatar_url":"https://github.com/AveroLLC.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"```\nnpm i types-first-ui\n```\n\n# Types-First UI\n\nTypes-First UI is an opinionated framework for building long-lived, maintainable UI codebases. It uses TypeScript and the power of its type system to guarantee the completeness and correctness of a Redux backend that can be connected to React components in a performant way. It places the focus on first defining your system in terms of the data and types that will drive it, then filling in the blanks. This project is inspired heavily by [re-frame](https://github.com/Day8/re-frame).\n\n## Who is this for?\n\nWell, first and foremost, it's for us at Avero. This library represents the codification and enforcement of what we have established as our best practices for building interfaces. However, we believe that there is significant value in this approach and that it is worth sharing.\n\nHopefully, there are others who can use and benefit from this work directly. But even if not, we think it is valuable to share our approach and give visibility into how one group of engineers chooses to approach some of the hard problems of building maintainable UI codebases.\n\nMore generally, this project might be for you if you believe...\n\n- Types are important\n- TypeScript is good\n- Flat state tree -- all reducers have access to the entire tree\n- Actions may have a reducer, an epic, both, or neither. M:N relationship between Actions =\u003e Redux primitives\n- Epics (backed by redux-observable) as mechanism for side effects/middleware (vs. thunks or sagas)\n- Observables are pretty dope\n\n## **Philosophy**\n\nRedux is an event dispatching system that operates on a single global state atom that is only modifiable via actions. It makes little sense to begin building your application before defining two interfaces: the shape of your state and the shape of all actions that operate on that state. From there we can leverage the strict unidirectional flow of Redux in our type system.\n\nYou should be familiar with the basic terminology of Redux (Actions, Reducers, Action Creators, Stores, Middleware) and RxJS (Observables, Epics) before reading further. The documentation for [Redux](https://redux.js.org/) and [Epics](https://redux-observable.js.org/docs/basics/Epics.html) are excellent, so we'd strongly encourage reading it.\n\nThis project aims to facilitate this \"types-first\" style of application design while providing utilities focused around maximizing type safety and maintaining interop with existing Redux libraries.\n\n## **Anti-Pitch**\n\nIf you dislike types \"getting in your way\" or think of them as secondary to your application, then this is not the framework for you. At Avero we believe in starting with your API first, which in this case is the interfaces of your application.\n\nIf you do not care about the long-term maintainability of your codebase, such as writing a todo app or a school project, this may be overkill for you. This framework is designed and optimized for building large, production UI projects that will have a long lifespan and many contributors.\n\nYou will occasionally run into cryptic error messages (e.g. key inference on Paths) due to relying heavily on type inference. Deep lookup types + mapped types + conditional types + inference makes the compiler work pretty hard... which leads to the next point.\n\nVS Code is a bit sluggish with this right now. You will not receive the instant 10ms feedback you may be used to, so there is a tradeoff here. Hopefully this will improve as the compiler matures around these newer type features. When I tried Webstorm it was not giving me the same level of inference as VS Code (Types-First UI requires TS 2.9+).\n\n## Usage\n\nHere's a diagram showing the following usage steps. Click to embiggen.\n\n\u003ca href=\"usage-diagram-lg.png?raw=true\"\u003e\u003cimg src=\"usage-diagram-sm.png\" /\u003e\u003c/a\u003e\n\nLet's create a Redux counter application with Types-First UI. We will include basic pending/error handling and a mock API call to show slightly more complexity than the normal counter app.\n\n```typescript\n// 1. Create State Tree interface / initialState\nexport interface State {\n  counter: number;\n  error: string;\n  pendingRequest: boolean;\n};\n\nexport const initialState: State = {\n  counter: 0,\n  error: '',\n  pendingRequest: false\n};\n\n// 2. Create ActionTypes enum\nexport enum ActionTypes = {\n  ADD_REQUEST = 'ADD_REQUEST',\n  ADD_FAIL = 'ADD_FAIL'\n  ADD_SUCCESS = 'ADD_SUCCESS'\n};\n\n// 3. Create Action interfaces\nexport interface Actions {\n  [ActionTypes.ADD_REQUEST]: { type: ActionTypes.ADD_REQUEST, payload: { tryAdd: number } };\n  [ActionTypes.ADD_FAIL]: { type: ActionTypes.ADD_FAIL, payload: { error: string } };\n  [ActionTypes.ADD_SUCCESS]: { type: ActionTypes.ADD_SUCCESS, payload: { newCount: number} };\n};\n\nexport type AppActions = Actions[keyof Actions]; // creates Union type of actions that we will pass in to createTypesafeRedux\n```\n\nThe first thing we want to do is define the interfaces of our application. The entire UI depends on state and action interfaces, so it makes sense to start with them first. Additionally, most maintenance work involves adding or modifying actions, so keeping them in one place is helpful for maintainability. Any changes to these interfaces should propagate down to the rest of the system. When we talk about \"unidirectional\" types, we are referring to these two interfaces driving downstream functions and utilities.\n\n```typescript\n// 4. Define any Epic Dependencies\nexport interface EpicDependencies {\n  // Represents API call\n  counterAddSvc: {\n    add: (tryAdd: number) =\u003e Observable\u003c{ newCount: number } | { error: string }\u003e;\n  };\n}\n```\n\nOur Redux application has little knowledge of the outside world. Its focus is around actions and how those actions affect the state atom. We use [Epics](#epics) as a primitive for side effects, but the logic of those side effects should live behind \"Services\", which generically encapsulate any API that returns an observable. This is helpful for testing and separation of concerns.\n\n```typescript\n// 5. Pass our interfaces to createTypesafeRedux\nimport { createTypesafeRedux } from 'types-first-ui';\n\nexport const { path, selector, action, createApp } = createTypesafeRedux\u003c\n  State,\n  AppActions,\n  EpicDependencies\n\u003e();\n```\n\nNow we can use these interfaces to drive our typesafe \"utility\" functions. These are the functions you will use to build your Redux application. Any changes to the interfaces described above will be immediately reflected in these function signatures.\n\n```typescript\n// 6a. Create paths using the 'path' utility function from createTypesafeRedux\nexport const Paths = {\n  counter: path(['counter'], 0),\n  error: path(['error'], ''),\n  pendingRequest: path(['pendingRequest']),\n};\n\n// 6b. Create selectors using 'selector' util function from createTypesafeRedux\nexport const doubleCounter = selector(Paths.counter, counter =\u003e {\n  return counter * 2;\n});\n```\n\nWe now use the utility functions from `createTypesafeRedux` to create our [Paths](#path) and [Selectors](#selector). Generically, these represent observables of derived values from your state tree. Specifically, Paths are a directly referenceable property on your state tree; Selectors are derived values that are computed as a function of input Paths and Selectors.\n\nAdditionally, Paths include utility get, set, \u0026 unset functions that will be used in your reducers.\n\n```typescript\nimport { flow } from 'types-first-ui';\n\n// 7. Implement Actions: use 'action' util function from createTypesafeRedux\nconst addRequest = action(ActionTypes.ADD_REQUEST, {\n  reducer: (state, action) =\u003e {\n    // action inferred as {type: ActionTypes.ADD_REQUEST, payload: {tryAdd: number} }\n    return Paths.pendingRequest.set(true)(state);\n  },\n  epic: (action$, { counterAddSvc }) =\u003e {\n    return action$.pipe(\n      mergeMap(action =\u003e {\n        // action inferred as {type: ActionTypes.ADD_REQUEST, payload: {tryAdd: number} }\n        return counterAddSvc.add(action.payload.tryAdd).pipe(\n          map(newCount =\u003e {\n            // API call was successful\n            return addSuccess.creator({ newCount });\n          }),\n          catchError(error =\u003e {\n            // API call failed\n            return addFail.creator({ error });\n          })\n        );\n      })\n    );\n  },\n});\n\nconst addSuccess = action(ActionTypes.ADD_SUCCESS, {\n  reducer: (state, action) =\u003e {\n    const togglePending = Paths.pendingRequest.set(false);\n    const updateCounter = Paths.counter.set(action.payload.newCount);\n    return flow(\n      togglePending,\n      updateCounter\n    )(state);\n  },\n});\n\nconst addFail = action(ActionTypes.ADD_FAIL, {\n  reducer: (state, action) =\u003e {\n    const togglePending = Paths.pendingRequest.set(false);\n    const updateError = Paths.error.set(action.payload.error);\n    return flow(\n      togglePending,\n      updateError\n    )(state);\n  },\n});\n\nexport const ActionsMap = {\n  [ActionTypes.ADD_REQUEST]: addRequest,\n  [ActionTypes.ADD_SUCCESS]: addSuccess,\n  [ActionTypes.ADD_FAIL]: addFail,\n};\n```\n\nThis is the primary focus of the developer when creating Redux applications. Given a set of actions and a state tree, we now need to implement the concrete instances of these actions.\nThis may include reducers, epics, both, or neither. The return type of `action` is an [ActionImplementation](#action-implementation). The exported ActionsMap represents an exhaustive collection of implementations for each action type that we defined in our initial interfaces. It will be passed into our `createApp` function below.\n\n```typescript\n// 8. Create the app instance\nconst app = createApp({\n  actions: ActionsMap,\n  initialState,\n});\n\n// 9. Initialize the app instance by creating \u0026 binding to a new redux store\napp.createStore({\n  epicDependencies: {\n    // concrete instance of interface\n    counterAddSvc: CounterAddSvc,\n  },\n  // enables redux dev tools\n  dev: true,\n});\n\nexport default app;\n```\n\nWith our concrete instances, we can now create our app instance. This is used to bridge React and Redux--the app exposes a `connect` function that mirrors the Redux variation, except with support for our path and selector primitives. We use an explicit import of the app rather than a `Provider` component using React's context API.\n\n```typescript\nimport { ActionCreator } from 'types-first-ui';\n// helper utility to extract ActionCreator type given the discriminant\n// useful to minimize boilerplate in ActionProps\ntype Creator\u003cT extends ActionTypes\u003e = ActionCreator\u003cExtract\u003cAppActions, { type: T }\u003e\u003e;\n\n// 10. Define a React component with Types-First framework\ninterface DataProps {\n  counter: number;\n  doubleCounter: number;\n}\n\ninterface ActionProps {\n  addRequest: Creator\u003cActionTypes.ADD_REQUEST\u003e;\n}\n\ntype Props = DataProps \u0026 ActionProps;\n\nexport class CounterComponent extends React.PureComponent\u003cProps\u003e {\n  add = () =\u003e {\n    this.props.addRequest({ tryAddBy: 1 });\n  };\n\n  render() {\n    return (\n      \u003cdiv\u003e\n        \u003cdiv\u003e{this.props.counter}\u003c/div\u003e\n        \u003cdiv\u003e{this.props.doubleCounter}\u003c/div\u003e\n        \u003cbutton onClick={this.add}\u003eAdd\u003c/button\u003e\n      \u003c/div\u003e\n    );\n  }\n}\n\n// 11. Connect your React component to the TFUI app\nconst observableProps = {\n  counter: Paths.COUNTER,\n  doubleCounter: Selectors.doubleCounter,\n};\n\nconst dispatchProps = {\n  addRequest: app.actionCreator(ActionTypes.COUNTER_ADD_REQUEST),\n};\n\nexport default app.connect\u003cDataProps, ActionProps\u003e(\n  observableProps,\n  dispatchProps\n)(CounterComponent);\n```\n\n## **Concepts**\n\n### Selector\n\nA selector is an observable representing some directly derivable value from your state atom. Selectors can be recursively combined to create other selectors. These are conceptually similar to [ngrx](https://github.com/ngrx/platform/blob/master/docs/store/selectors.md) selectors, as well as [reselect](https://github.com/reduxjs/reselect).\n\nSelectors are closed over an observable of the state tree, and include a number of performance optimizations to guarantee that they will be shared, they will not leak subscriptions, they will emit at most once per change to the state tree, and they will only evaluate their projector function when their input values change. Selectors may optionally provide a comparator function to determine when new values should be emitted, for further performance optimizations. This is useful for selectors that return values which are not referentially equal (i.e. mapping over an array).\n\nSelectors are created using the `selector` utility function.\n\n```typescript\ninterface Todo {\n  text: string;\n  completed: boolean;\n}\n\ninterface State {\n  app: {\n    todos: {\n      [todoId: string]: Todo;\n    };\n  };\n}\n\nexport const Paths = {\n  TODOS: path(['app', 'todos']),\n};\n\n// Example selector usage--usually more than one input selector\nexport const completedTodos = selector(Paths.TODOS, todos =\u003e {\n  return _.pickBy(todos, todo =\u003e {\n    return todo.completed;\n  });\n});\n// completedTodos is type Observable\u003c[todoId: string]: Todo\u003e\n\n// selectors may also be parameterized\n// Here is an example that returns a selector curried over a parameter\nexport const todoById = (id: string) =\u003e selector(Paths.TODOS, todos =\u003e todos[id]);\n```\n\n### Path\n\n```typescript\nexport interface PathAPI\u003cTState extends object, TVal\u003e {\n  get: (state: TState) =\u003e TVal;\n  set: (nextVal: TVal) =\u003e (state: TState) =\u003e TState;\n  unset: (state: TState) =\u003e TState;\n}\nexport declare type Path\u003cTState extends object, TVal\u003e = Selector\u003cTVal\u003e \u0026\n  PathAPI\u003cTState, TVal\u003e;\n```\n\nA path is a selector with special properties and constraints. Paths represent observables of some subtree of your state atom. A path is constructed by providing the literal path to a subtree of your state interface. It is an observable that will emit whenever the focused piece of the state tree has changed (i.e. it is no longer referentially equal to its previous value). Paths are the primitive from which we compute other derived values, through selectors. For every subtree of your state tree, there should be a corresponding Paths object.\n\nBecause paths are bound directly to a piece of the state tree, they also include typesafe, non-mutating get, set, and unset functions which are used in our reducers.\n\nPaths are created using the `path` utility function.\n\n```typescript\n// Example use of path\ninterface State {\n  app: {\n    counter: number;\n    username: string;\n    counterById: {\n      [counterId: string]: number;\n    };\n  };\n}\n\nexport const Paths = {\n  COUNTER: path(['app', 'counter'], 0), // optional default argument,\n  // Paths.COUNTER is type Observable\u003cnumber\u003e \u0026 { get: (State) =\u003e number, set: (number) =\u003e StateTransform\u003cState\u003e, unset: StateTransform\u003cState\u003e }\n  USERNAME: path(['app', 'username'], ''),\n  // Paths.USERNAME is type Observable\u003cstring\u003e \u0026 { get: (State) =\u003e string, set: (string) =\u003e StateTransform\u003cState\u003e , unset: StateTransform\u003cState\u003e}\n  BAD: path(['app', 'badPath']),\n  // ERROR: argument of 'badPath' is not assignable to 'counter' | 'username'\n};\n```\n\n### Action Implementation\n\n```typescript\nexport interface ActionImplementation\u003c\n  TAction extends TAllActions,\n  TState extends object,\n  TAllActions extends Action,\n  TEpicDependencies extends object\n\u003e {\n  constant: TAction['type'];\n  creator: ActionCreator\u003cTAction\u003e;\n  reducer?: IReducer\u003cTState, TAction\u003e;\n  epic?: SingleActionEpic\u003cTAllActions, TAction, TEpicDependencies\u003e;\n}\n```\n\nThis interface represents the \"implementation\" of a single action described in the system. It is the return type of the `action` function provided the framework. This provides strict type safety around your Redux primitives, as well as providing a useful grouping of tightly related code. The community has referred to this pattern as the [ducks](https://github.com/erikras/ducks-modular-redux) pattern. Action implementations are where we combine our typesafe utilities into meaningful business logic that will drive the functionality of our application.\n\nActions are implemented using the `action` utility function. When implementing an action you may provide a reducer or an epic for that action; you must exhaustively implement every action defined in your action types before you will be able to create an app instance.\n\n```typescript\n// the return value of the action utility is the full action implementation including\n// a typesafe creator function and reference to the type constant\nconst addRequest = action(ActionTypes.ADD_REQUEST, {\n  // reducers receive the full, flat state tree\n  // type of action object is correctly inferred\n  reducer: (state, action) =\u003e {\n    // We use our path to return an update state tree\n    return Paths.pendingRequest.set(true)(state);\n  },\n  // action epics are scoped to the specific action being implemented\n  // notice there is no need to use ofType() here, because it is already\n  // a stream of the ActionTypes.ADD_REQUEST action\n  epic: (action$, { counterAddSvc }) =\u003e {\n    return action$.pipe(\n      // type of action object is correctly inferred\n      mergeMap(action =\u003e {\n        return counterAddSvc.add(action.payload.tryAdd).pipe(\n          map(newCount =\u003e {\n            // API call was successful\n            return addSuccess.creator({ newCount });\n          }),\n          catchError(error =\u003e {\n            // API call failed\n            return addFail.creator({ error });\n          })\n        );\n      })\n    );\n  },\n});\n```\n\n### Epics\n\nTypes-First UI is built on top of redux-observable. We use epics as the primitive for managing asynchrony and side-effects within our system.\n\nIn general, epics meet the contract of actions in, actions out. For a full exploration of the concept, you should refer to the [redux observable guide](https://redux-observable.js.org/docs/basics/Epics.html). However, we also provide a set of more constrained, specific epic definitions: single-action epics, middleware.\n\n```typescript\nexport declare type Epic\u003c\n  TWatchedAction extends Action,\n  TReturnedAction extends Action,\n  TEpicDependencies extends object\n\u003e = (\n  action$: Observable\u003cTWatchedAction\u003e,\n  deps: TEpicDependencies\n) =\u003e Observable\u003cTReturnedAction\u003e;\n```\n\n#### Single-Action Epic\n\nSingle-action epics are provided as part of an action implementation. They are scoped to the specific action being implemented, and only have access to the stream of all actions emitted from the system as the third parameter. As a user, this means you do not have to use the `ofType()` operator within your action epics to narrow the action stream. This also guarantees that action implementations do not bleed concerns. Although most single action epics won't care about the allActions stream, it is useful for use cases such as cancellation.\n\n```typescript\nexport declare type SingleActionEpic\u003c\n  TAllActions extends Action,\n  TAction extends TAllActions,\n  TEpicDependencies extends object\n\u003e = Epic\u003cTAction, TAllActions, TEpicDependencies\u003e;\n```\n\n```typescript\nconst addRequest = action(ActionTypes.ADD_REQUEST, {\n  // action epics are scoped to the specific action being implemented\n  // notice there is no need to use ofType() here, because it is already\n  // a stream of the ActionTypes.ADD_REQUEST action\n  epic: action$ =\u003e {\n    return action$.pipe(\n      // type of action object is correctly inferred\n      map(action =\u003e {\n        //...\n      })\n    );\n  },\n});\n```\n\n#### Middleware\n\nNot to be confused with [Redux middleware](https://redux.js.org/advanced/middleware), this represents a particular type of \"Epic\"--one that never returns new actions. This is a good place to put \"side effects\" in your application (logging, tracing, setting context/storage, persistence, etc).\n\n```typescript\nexport declare type MiddlewareEpic\u003c\n  TAllActions extends Action,\n  TEpicDependencies extends object\n\u003e = Epic\u003cTAllActions, never, TEpicDependencies\u003e;\n```\n\nFor example, we could have:\n\n```typescript\nimport { empty } from 'rxjs';\nimport { tap, mergeMapTo } from 'rxjs/operators';\n\nconst logEverything: Middleware\u003cAppActions, State, EpicDependencies\u003e = actions$ =\u003e {\n  return actions$.pipe(\n    tap(console.log),\n    mergeMapTo(empty())\n  );\n};\n```\n\n### App\n\nApp is the atomic unit of functionality in Types-First UI. Apps are recursively composable, which is cool...but scary and complicated. So I'm going to wait to document this feature.\n\n## Foundational Technologies\n\n- [TypeScript](https://github.com/Microsoft/TypeScript)\n- [Redux](https://github.com/reactjs/redux)\n- [RxJS](https://github.com/ReactiveX/rxjs)\n- [Redux-Observable](https://redux-observable.js.org/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAveroLLC%2Ftypes-first-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAveroLLC%2Ftypes-first-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAveroLLC%2Ftypes-first-ui/lists"}