Type safe utils for redux actions, epics, effects, react/preact default props, various type guards and TypeScript utils, React util components
MIT License
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 ? πͺβ
yarn add @martin_hotell/rex-tils
# OR
npm install @martin_hotell/rex-tils
Note:
- This library supports only
TS >= 3.1
( because it uses conditional types and generic rest arguments #dealWithIt )- For leveraging Rx
ofType
operator within your Epics/Effects you need to installrxjs>= 6.x
Let's demonstrate simple usage with old good Counter example:
// 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>
// 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
}
}
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())
)
Go checkout examples !
rex-tils API is tiny and consist of 2 categories:
createAction<T extends string,P>(type: T,payload?: P): Action<T,P>
ofType(...keys:string[]): Observable<Action>
isBlank(value:any)
isPresent(value:any)
isEmpty<T extends string | object>(value:T): Empty<T>
isFunction(value:any)
isBoolean(value:any)
isString(value:any)
isNumber(value:any)
isArray(value:any)
isObject<T>(value:T): T
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>
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
// $ExpectType (string | number | boolean)[]
const testWidened = ['one', 1, false]
// $ExpectType [string, number, boolean]
const testProperTuple = tuple('one', 1, false)
isEmptyChildren( children: ReactNode )
ChildrenAsFunction<T extends AnyFunction>( children: T ): T
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[] )
Props
generic props intersectionPickedProps
props type from which you wanna pick properties so you get them via destructuringtype 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>
createPropsGetter<T>(props: T): T
Why ?
https://medium.com/@martin_hotell/react-typescript-and-defaultprops-dilemma-ca7f81c661c7
DefaultProps
helper/type alias// $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>
)
}
}
<Pre/>
ActionsUnion<A extends StringMap<AnyFunction>> = ReturnType<A[keyof A]>
type Actions = ActionsUnion<typeof Actions>
ActionsOfType<ActionUnion, ActionType extends string>
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
Function
type constructorStringMap<T> = { [key: string]: T }
type Users = StringMap<{ name: string; email: string }>
const users: Users = {
1: { name: 'Martin', email: '[email protected]' },
2: { name: 'John', email: '[email protected]' },
}
Constructor<T>
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>
NonPrimitive<T>
Nullable<T>
NonNullable
Maybe<T>
InstanceTypes<T>
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>
for more info check this excellent [blog post about nominal typing in TypeScript](kudos to https://michalzalecki.com/nominal-typing-in-typescript/#approach-4-intersection-types-and-brands)
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>
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 issueKnownKeys<T>
[key:string]: any
RequiredKnownKeys<T>
[key:string]: any
OptionalKnownKeys<T>
[key:string]: any
PickWithTypeUnion<Base, Condition>
NOTE: It doesn't work for undefined | null values. for that use
PickWithType
PickWithType<Base, Condition>
null | undefined | object | string | number | boolean
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 inReact.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>
@TODO
Execute yarn release
which will handle following tasks:
releases are handled by awesome standard-version
1.1.2
to 1.1.2-0
:yarn release --prerelease
1.1.2
to 1.1.2-alpha.0
:yarn release --prerelease alpha
1.1.2
to 1.1.2-beta.0
:yarn release --prerelease beta
See what commands would be run, without committing to git or updating files
yarn release --dry-run
yarn pack
OR yarn release:preflight
which will create a tarball with everything that would get published to NPMTest are written and run via Jest πͺ
yarn test
# OR
yarn test:watch
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 ?
#Format and fix lint errors
yarn ts:style:fix
yarn docs
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
MIT as always