Giter Club home page Giter Club logo

rex-tils's Introduction

rex-tils ๐Ÿฆ– โš› ๏ธ๐Ÿ––

Type safe utils for redux actions and various guard utils for React and Angular

WHY/WHAT? ๐Ÿ‘‰ https://medium.com/@martin_hotell/improved-redux-type-safety-with-typescript-2-8-2c11a8062575

Enjoying/Using rex-tils ? ๐Ÿ’ชโœ…

Greenkeeper badge

Build Status NPM version Downloads Standard Version styled with prettier Conventional Commits

Installing

yarn add @martin_hotell/rex-tils
# OR
npm install @martin_hotell/rex-tils

Note:

  1. This library supports only TS >= 3.1 ( because it uses conditional types and generic rest arguments #dealWithIt )
  2. For leveraging Rx ofType operator within your Epics/Effects you need to install rxjs>= 6.x

Getting started

Let's demonstrate simple usage with old good Counter example:

Edit counter-example

  1. Create Type-safe Redux Actions
// actions.ts
import { ActionsUnion, createAction } from '@martin_hotell/rex-tils'

export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
export const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD'

export const Actions = {
  increment: () => createAction(INCREMENT),
  decrement: () => createAction(DECREMENT),
  incrementIfOdd: () => createAction(INCREMENT_IF_ODD),
}

// we leverage TypeScript token merging, so our consumer can use `Actions` for both runtime and compile time types ๐Ÿ’ช
export type Actions = ActionsUnion<typeof Actions>
  1. Use Type-safe Redux Actions within Reducer
// reducer.ts
import * as fromActions from './actions'

export const initialState = 0 as number
export type State = typeof initialState

export const reducer = (
  state = initialState,
  action: fromActions.Actions
): State => {
  switch (action.type) {
    case fromActions.INCREMENT: {
      // $ExpectType 'INCREMENT'
      const { type } = action

      return state + 1
    }
    case fromActions.DECREMENT: {
      // $ExpectType 'DECREMENT'
      const { type } = action

      return state - 1
    }
    default:
      return state
  }
}
  1. Use Type-safe Redux Actions within Epics with ofType Rx operator
// epics.ts

import { ofType } from '@martin_hotell/rex-tils'
import { ActionsObservable, StateObservable } from 'redux-observable'
import { filter, map, withLatestFrom } from 'rxjs/operators'

import * as fromActions from './actions'
import { AppState } from './store'

export const incrementIfOddEpic = (
  // provide all our Actions type that can flow through the stream
  // everything else is gonna be handled by TypeScript so we don't have to provide any explicit type annotations. Behold... top notch DX ๐Ÿ‘Œโค๏ธ๐Ÿฆ–
  action$: ActionsObservable<fromActions.Actions>,
  state$: StateObservable<AppState>
) =>
  action$.pipe(
    ofType(fromActions.INCREMENT_IF_ODD),
    withLatestFrom(state$),
    filter(
      (
        [action, state] // $ExpectType ['INCREMENT_IF_ODD', {counter:number}]
      ) => state.counter % 2 === 1
    ),
    map(() => fromActions.Actions.increment())
  )

Examples

Go checkout examples !

API

rex-tils API is tiny and consist of 2 categories:

  1. Runtime JavaScript helpers
  2. Compile time TypeScript type helpers

1. Runtime javascript helpers

createAction<T extends string,P>(type: T,payload?: P): Action<T,P>

  • use for declaring action creators

ofType(...keys:string[]): Observable<Action>

  • use within Epic/Effect for filtering actions

Type guards:

  • all following type guards properly narrow your types ๐Ÿ’ช:

isBlank(value:any)

  • checks if value is null or undefined

isPresent(value:any)

  • checks if value is not null nor undefined

isEmpty<T extends string | object>(value:T): Empty<T>

  • checks if value is empty for string | array | object otherwise it throws an error.
  • also it narrows the type to empty type equivalent

isFunction(value:any)

isBoolean(value:any)

isString(value:any)

isNumber(value:any)

isArray(value:any)

isObject<T>(value:T): T

  • normalized check if JS value is an object. That means anything that is not an array, not null/undefined but typeof value equals to 'object'
  • it will also properly narrow type within the branch
type MyMap = { who: string; age: number }
declare const someObj: MyMap | string | number

if (isObject(someObj)) {
  // $ExpectType MyMap
  someObj
} else {
  // $ExpectType string | number
  someObj
}

isDate(value:any): value is Date

isPromise(value:any): value is PromiseLike<any>

Utils:

noop(): void

identity<T>(value:T):T

Enum(...tokens:string[]): object

As described in 10 TypeScript Pro tips article, we don't recommend to use enum feature within your codebase. Instead you can leverage this small utility function which comes as both function and type alias to get proper enum object map and type literal, if you really need enums in runtime.

// enums.ts

// $ExpectType Readonly<{ No: "No"; Yes: "Yes"; }>
 export const AnswerResponse = Enum('No', 'Yes')
 // $ExpectType 'No' | 'Yes'
 export type AnswerResponse = Enum(typeof AnswerResponse)

 // consumer.ts

 import {AnswerResponse} from './enums'

 export const respond = (
   recipient: string,
   // 1. ๐Ÿ‘‰ enum used as type
   message: AnswerResponse
  ) => { /*...*/}

 // usage.ts

 import {respond} from './consumer'
 import {AnswerResponse} from './enums'

 respond('Johnny 5','Yes')
 respond(
   'Johnny 5',
   // 2. ๐Ÿ‘‰  enum used as reference
   AnswerResponse.No
   )

tuple(...args: T): T

  • Implicitly create a tuple with proper tuple types instead of widened array of union types
// $ExpectType (string | number | boolean)[]
const testWidened = ['one', 1, false]

// $ExpectType [string, number, boolean]
const testProperTuple = tuple('one', 1, false)

React/Preact related helpers:

isEmptyChildren( children: ReactNode )

  • checks if Children.count === 0

ChildrenAsFunction<T extends AnyFunction>( children: T ): T

  • similar to Children.only although checks if children is only a function. Useful for children as a function pattern. If not will throw an error otherwise narrows children type to function and returns it.
type Props = {
  userId: string
  children: (props: { data: UserModel }) => ReactElement
}

type State = { data: UserModel | null }

class UserRenderer extends Component<Props, State> {
  render() {
    const { data } = this.state
    // Will throw on runtime if children is not a function
    // $ExpectType (props: {data: UserModel}) => ReactElement
    const childrenFn = ChildrenAsFunction(children)

    return data ? children(data) : 'Loading...'
  }

  componentDidMount() {
    fetch(`api/users/${this.props.userId}`)
      .json()
      .then((data) => this.setState({ data }))
  }
}

const App = () => (
  <UserRenderer userId={7}>
    {({ data }) => <div>name: {data.name}}</div>}
  </UserRenderer>
)

pickWithRest<Props, PickedProps>( props: object, pickProps: keyof PickedProps[] )

  • use for getting generic ...rest from object ( TS cannot do that by default )
  • you need to explicitly state generic params
  • Props generic props intersection
  • PickedProps props type from which you wanna pick properties so you get them via destructuring
type InjectedProps = { one: number; two: boolean }
function test<OriginalProps>(props: OriginalProps) {
  type Props = OriginalProps & InjectedProps
  const {
    // $ExpectType number
    one,
    // $ExpectType OriginalProps
    rest,
  } = pickWithRest<Props, InjectedProps>(props, ['one'])
}

DefaultProps<T>(props: T): Readonly<T>

  • returns frozen object ( useful for default props )

createPropsGetter<T>(props: T): T

Why ?

https://medium.com/@martin_hotell/react-typescript-and-defaultprops-dilemma-ca7f81c661c7

  • use for resolving defaultProps within component implementation
  • goes side by side with DefaultProps helper/type alias
  • it's just identity function with proper props type resolution
// $ExpectType {onClick: (e: MouseEvent<HTMLElement>) => void, children: ReactNode, color?:'blue' | 'green' | 'red', type?: 'button' | 'submit'}
type Props = {
  onClick: (e: MouseEvent<HTMLElement>) => void
  children: ReactNode
} & DefaultProps<typeof defaultProps>

// $ExpectType Readonly<{color:'blue' | 'green' | 'red', type: 'button' | 'submit'}>
const defaultProps = DefaultProps({
  color: 'blue' as 'blue' | 'green' | 'red',
  type: 'button' as 'button' | 'submit',
})
const getProps = createPropsGetter(defaultProps)

class Button extends Component<Props> {
  static readonly defaultProps = defaultProps
  render() {
    const {
      // $ExpectType (e: MouseEvent<HTMLElement>) => void
      onClick: handleClick,
      // $ExpectType 'blue' | 'green' | 'red'
      color,
      // $ExpectType 'button' | 'submit'
      type,
      // $ExpectType ReactNode
      children,
    } = getProps(this.props)

    return (
      <button onClick={handleClick} type={type} className={color}>
        {children}
      </button>
    )
  }
}

React/Preact components:

<Pre/>

  • for debugging data within your render

2. Compile time TypeScript type helpers

ActionsUnion<A extends StringMap<AnyFunction>> = ReturnType<A[keyof A]>

  • use for getting action types from action creators implementation
type Actions = ActionsUnion<typeof Actions>

ActionsOfType<ActionUnion, ActionType extends string>

  • helper for getting particular action type from ActionsUnion
const SET_AGE = '[core] set age'
const SET_NAME = '[core] set name'

const Actions = {
  setAge: (age: number) => createAction(SET_AGE, age),
  setName: (name: string) => createAction(SET_NAME, name),
}

type Actions = ActionsUnion<typeof Actions>

type AgeAction = ActionsOfType<Actions, typeof SET_AGE>

const action: AgeAction = {
  type: '[core] set age',
  payload: 23,
}

AnyFunction = (...args: any[]) => any

  • use this type definition instead of Function type constructor

StringMap<T> = { [key: string]: T }

  • simple alias to save you keystrokes when defining JS typed object maps
type Users = StringMap<{ name: string; email: string }>

const users: Users = {
  1: { name: 'Martin', email: '[email protected]' },
  2: { name: 'John', email: '[email protected]' },
}

Constructor<T>

  • alias for the construct signature that describes a type which can construct objects of the generic type T and whose constructor function accepts an arbitrary number of parameters of any type

Omit<T,K>

type Result = Omit<
  {
    one: string
    two: number
    three: boolean
  },
  'two'
>

const obj: Result = {
  one: '123',
  three: false,
}

Diff<T extends object,K extends object>

type Result = Diff<
  {
    one: string
    two: number
    three: boolean
  },
  {
    two: number
  }
>

const obj: Result = {
  one: '123',
  three: false,
}

Primitive<T>

  • narrows type to primitive JS value ( boolean, string, number, symbol )

NonPrimitive<T>

  • narrows type to non-primitive JS value ( object, function, array )

Nullable<T>

  • opposite of standard library NonNullable

Maybe<T>

  • Maybe types accept the provided type as well as null or undefined

InstanceTypes<T>

  • obtain the return type of a constructor function type within array or object.

    Like native lib.d.ts InstanceType but for arrays/tuples or objects

class Foo {
  hello = 'world'
}
class Moo {
  world = 'hello'
}

const arr: [typeof Foo, typeof Moo] = [Foo, Moo]
const obj: { foo: typeof Foo; moo: typeof Moo } = { foo: Foo, moo: Moo }

// $ExpectType [Foo, Moo]
type TestArr = InstanceTypes<typeof arr>

// $ExpectType {foo: Foo, moo: Moo}
type TestObj = InstanceTypes<typeof obj>

Brand<T,K>

type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>

const usd = 10 as USD
const eur = 10 as EUR

function gross(net: USD, tax: USD): USD {
  return (net + tax) as USD
}

gross(usd, usd) // ok
gross(eur, usd) // Type '"EUR"' is not assignable to type '"USD"'.

UnionFromTuple<T>

  • extracts union type from tuple

FunctionArgsTuple<T>

@DEPRECATED ๐Ÿ‘‰ Instead use standard library Parameters mapped type

This is useful with React's children as a function(render prop) pattern, when implementing HoC

const funcTestOneArgs = (one: number) => {
  return
}
// $ExpectType [number]
type Test = FunctionArgsTuple<typeof funcTestNoArgs>

Values<T>

  • Values<T> represents the union type of all the value types of the enumerable properties in an object Type T.
type Props = {
  name: string
  age: number
}

// The following two types are equivalent:
// $ExpectType string | number
type Prop$Values = Values<Props>

// $ExpectType string
const name: Prop$Values = 'Jon'
// $ExpectType number
const age: Prop$Values = 42

Keys<T>

  • keyof doesn't work/distribute on union types. This mapped type fixes this issue

KnownKeys<T>

  • gets proper known keys from object which contains index type [key:string]: any

RequiredKnownKeys<T>

  • gets required only known keys from object which contains index type [key:string]: any

OptionalKnownKeys<T>

  • gets optional only known keys from object which contains index type [key:string]: any

PickWithTypeUnion<Base, Condition>

  • Pick key-values from Base provided by Condition generic type. Generic can be an union.

NOTE: It doesn't work for undefined | null values. for that use PickWithType

PickWithType<Base, Condition>

  • Pick key-values from Base provided by Condition generic type. Generic needs to be one type from null | undefined | object | string | number | boolean

React related types:

ElementProps<T>

Gets the props for a React element type, without preserving the optionality of defaultProps. Type could be a React class component or a stateless functional component. This type is used for the props property on React.Element<typeof Component>.

Like React.Element<typeof Component>, Type must be the type of a React component, so you need to use typeof as in React.ElementProps<typeof MyComponent>.

NOTE: Because ElementProps does not preserve the optionality of defaultProps, ElementConfig (which does) is more often the right choice, especially for simple props pass-through as with higher-order components.

import React from 'react'

class MyComponent extends React.Component<{ foo: number }> {
  render() {
    return this.props.foo
  }
}

;({ foo: 42 } as ElementProps<typeof MyComponent>)

ElementConfig<T>

Like ElementProps<typeof Component> this utility gets the type of a componentโ€™s props but preserves the optionality of defaultProps!

Like React.Element, Type must be the type of a React component so you need to use typeof as in React.ElementProps.

import React from 'react'
class MyComponent extends React.Component<{ foo: number }> {
  static defaultProps = { foo: 42 }
  render() {
    return this.props.foo
  }
}

// `ElementProps<>` requires `foo` even though it has a `defaultProp`.
;(({ foo: 42 } as ElementProps<typeof MyComponent>)(
  // `ElementConfig<>` does not require `foo` since it has a `defaultProp`.
  {} as ElementConfig<typeof MyComponent>
))
type Props = { who: string }
type State = { count: number }
class Test extends Component<Props, State> {}
const TestFn = (_props: Props) => null
const TestFnViaGeneric: SFC<Props> = (_props) => null

// $ExpectType {who: string}
type PropsFromComponent = GetComponentProps<Test>

// $ExpectType {who: string}
type PropsFromFunction = GetComponentProps<typeof TestFn>

// $ExpectType {who: string}
type PropsFromFunction2 = GetComponentProps<typeof TestFnViaGeneric>

ElementState<T>

Gets Component/PureComponent state type

class MyComponent extends React.Component<{}, { foo: number }> {
  state = { foo: 42 }
  render() {
    return this.props.foo
  }
}

// $ExpectType {foo: number}
type State = ElementState<typeof MyComponent>

DefaultProps<T>(props: T): Partial<T>

  • type alias
  • useful for declaring Component props intersection with defaultProps

Guides

@TODO


Publishing

Execute yarn release which will handle following tasks:

  • bump package version and git tag
  • update/(create if it doesn't exist) CHANGELOG.md
  • push to github master branch + push tags
  • publish build packages to npm

releases are handled by awesome standard-version

Pre-release

  • To get from 1.1.2 to 1.1.2-0:

yarn release --prerelease

  • Alpha: To get from 1.1.2 to 1.1.2-alpha.0:

yarn release --prerelease alpha

  • Beta: To get from 1.1.2 to 1.1.2-beta.0:

yarn release --prerelease beta

Dry run mode

See what commands would be run, without committing to git or updating files

yarn release --dry-run

Check what files are gonna be published to npm

  • yarn pack OR yarn release:preflight which will create a tarball with everything that would get published to NPM

Tests

Test are written and run via Jest ๐Ÿ’ช

yarn test
# OR
yarn test:watch

Style guide

Style guides are enforced by robots, I meant prettier and tslint of course ๐Ÿค– , so they'll let you know if you screwed something, but most of the time, they'll autofix things for you. Magic right ?

Style guide npm scripts

#Format and fix lint errors
yarn ts:style:fix

Generate documentation

yarn docs

Commit ( via commitizen )

  • this is preferred way how to create conventional-changelog valid commits
  • if you prefer your custom tool we provide a commit hook linter which will error out, it you provide invalid commit message
  • if you are in rush and just wanna skip commit message validation just prefix your message with WIP: something done ( if you do this please squash your work when you're done with proper commit message so standard-version can create Changelog and bump version of your library appropriately )

yarn commit - will invoke commitizen CLI

Troubleshooting

Licensing

MIT as always

rex-tils's People

Contributors

hotell avatar navneet-g avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rex-tils's Issues

Proposal implementation of handleActions

Really nice solution for typing Redux! It's exactly what I've been looking for for a while! Thanks! ๐Ÿ˜„

Feature request proposal

Use case(s)

One thing I really like from redux-actions is handleActions. For me it's much cleaner than switch statements.

The problem is it doesn't play well with rex-tils's approach, which is much better IMHO.

I created this version of handleActions:

const handleActions = <
  State,
  Types extends string,
  Actions extends ActionsUnion<{ [T in Types]: AnyFunction }>
>(
  handler: {
    [T in Types]: (state: State, action: ActionsOfType<Actions, T>) => State
  },
  initialState: State,
) => (state = initialState, action: Actions): State =>
  handler[action.type] ? handler[action.type](state, action) : state;

Which can be used like this:

interface State {
  foo: boolean,
  bar: number,
}

enum Types {
  FOO = 'FOO',
  BAR = 'BAR',
}

const Actions = {
  foo: () => createAction(Types.FOO),
  bar: (bar: number) => createAction(Types.FOO, bar),
}

const reducer = handleActions<State, Types, Actions>(
  {
    [Types.FOO]: (state) => ({ ...state, foo: !state.foo }),
    [Types.BAR]: (state, { payload }) => ({ ...state, bar: payload, }),
  },
  { foo: true, bar: 0 },
)

Enums are not necessary, it works with constants but requires a few lines more:

...

const FOO = 'FOO'
const BAR = 'BAR'

type Types =
  | typeof FOO
  | typeof BAR

...

Props

  • Works as the switch version, type of action is inferred for each case, but much less boilerplate
  • The handler object expects all the types to be "handled" (i.e. you get a type error if one of the action types is not covered)

Cons

  • handleActions expects 3 type parameters. I tried to find a way to make it work only with State and Actions but I'm not sure is possible. Maybe I'm missing something.

If you think this is worth adding to rex-tils I can create a Pull Request.

Edit handle-actions

Redux-saga compatibility

Hi @Hotell, thanks a lot for the library. It provides the best typesafe Redux + Typescript FSA solution (as far as I can see).

Unfortunately it does not work with Redux-saga as expected.

The problem is createAction() function returns frozen object in DEV environment, but Saga does Object.defineProperty(action, SAGA_ACTION, { value: true }) which leads to an exception:

TypeError: Cannot define property @@redux-saga/SAGA_ACTION, object is not extensible

For now I solve this problem by 'unfreezing' the action: yield put({...Actions.fetchSuccess(data)}).

But my question is do we really need to return frozen action object with createAction()? I understand it provides more 'safe-to-be-used' FSA, but on the other hand this solution conflicts with quite a popular Redux tool.

feature request: Enum type with key/values

Feature request

Ability to pass key/value pairs to Enum

Use case(s)

Creating an enum of error response codes:

Enum({ JWT_INVALID: 31003, JWT_EXPIRED: 31004 });

The current implementation of Enum just mirrors the string, but if I am trying to type some other value, like a number, I can't create a nice readable mapping e.g.

if (errorCode === Errors.JWT_INVALID) // do something

Get the type of a single action

I find these utilities very useful in most cases, so thanks for putting them out there!

My project makes use of the redux-saga library. One issue we run into frequently is the ability to get the type of a single action, instead of the union of all the actions. For example, our saga code might look like this:

/// FooActions.ts
export const enum FooActionTypes {
    FooAction = "FooAction"
}

export const FooActions = {
    fooAction: (payload: IComplexPayload) => createAction(FooActionTypes.FooAction, payload)
}

export type FooActions = ActionsUnion<typeof FooActions>;


// Saga.ts
yield takeEvery(FooActionTypes.fooAction, processFooAction)

/*!!! can't type this properly, the payload type can be anything */
function* processFooAction(action: ActionWithPayload<FooActionTypes.FooAction, IWrongPayload> ) {
    ...
}

I would like to type the FooAction directly.
Do you have any suggestions on how I might accomplish this?

Type action creators

Feature request

How could I type Action creators?

Use case(s)

When passing actions as props, I need to manually type action creators:

interface LocalState {
  loadCredentials: () => void,
  loadCredentialsOk: () => void,
  loadCredentialsKo: (error: string) => void,
}

const mapDispatchToProps = {
  loadCredentials: Actions.loadCredentials,
  loadCredentialsOk: Actions.loadCredentialsOk,
  loadCredentialsKo: Actions.loadCredentialsKo,
}

PD: Awesome helpers for redux, thank you so much!!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.