Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type safety in effects, feedback #965

Closed
michaelgeorgeattard opened this issue Apr 9, 2018 · 2 comments
Closed

Type safety in effects, feedback #965

michaelgeorgeattard opened this issue Apr 9, 2018 · 2 comments

Comments

@michaelgeorgeattard
Copy link

michaelgeorgeattard commented Apr 9, 2018

Hi,

Our actions are declared as follows:

export enum AuthActionType {
	login = "[Auth] Login"
}

export class AuthAction {

	login(payload: LoginRequest): ActionWithPayload<AuthActionType.login, LoginRequest> {
		return {
			type: AuthActionType.login,
			payload
		};
	}
}

Each action class has a union of actions as follows:

export type AuthActions = ActionsUnion<AuthAction>;

ActionsUnion type is declared as follows:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

interface ActionCreatorsMapsObject { [actionCreator: string]: (...args: any[]) => any; }

type Union<A extends ActionCreatorsMapsObject> = ReturnType<A[keyof A]>;

type Filter<T, U> = T extends U ? T : never;

export type ActionsUnion<T extends Dictionary<any>> = Union<FunctionProperties<T>>;

This provides us with type safety in reducers without a lot of boilerplate code.

For effects we do the following for type safety:

@Effect() login$: Observable<Action> = this.actions$
	.ofType<ActionSignature<AuthActions, [AuthActionType.login]>>(AuthActionType.login)
	.pipe(
		switchMap(action => this.service.login(action.payload)
			.pipe(
				map(response => this.action.loginSuccess({
				   ...
				})),
				catchError((error: AppError) => of(this.action.loginFail(error)))
			)
		)
	);

ActionSignature type is declared as follows:

export type ActionSignature<T extends Action<any>, U extends T["type"][]> =
	(T extends ActionWithPayload<U[number], infer Z>
		? T
		: never)
	|
	(
		T extends ActionWithPayload<any, any>
		? never
		: T extends Action<infer Z> ? Filter<Z, U[number]> extends never ? never : Action<Filter<Z, U[number]>> : never
	);

We have a generic Action type, and an ActionWithPayload type as follows:

export interface Action<T extends string> {
	type: T;
}

export interface ActionWithPayload<T extends string, K> extends Action<T> {
	payload: K;
}

These types depend on Typescript 2.8+.

Can you give us feedback on our type safety solution?

Do you think it would be possible to move some of the type magic to ngrx?

@brandonroberts
Copy link
Member

See #860

@nasreddineskandrani
Copy link
Contributor

nasreddineskandrani commented Apr 27, 2018

@michaelgeorgeattard your stuff is so complex i think :)

look at the ngrx example
https://github.com/ngrx/platform/tree/master/example-app
do you think it's not good enof? or this doesn't support one of your cases?

//auth.type.ts

Credentials {
username: string;
passowrd: string
}

// auth.action.ts

import { Credentials } from './auth.type';

export const LOGIN = '[AuthActions] Login';
export const LOGOUT = '[AuthActions] Logout';

export class Login implments Action { // Action from ngrx
 public type: string = LOGIN;
 constructor(public payload: Credentials) {

}

export class Logout implments  Action { // Action from ngrx
 public type: string = LOGOUT;
 constructor(public payload: boolean) {

}

export const All = Login | Logout;

// auth.effect.ts

import * as AuthActions from'./auth.action';

@Effect() login$: Observable<Action> = this.actions$
	.ofType<AuthActions.Login>(AuthActions.LOGIN)
	.pipe(
		switchMap(action => this.service.login(action.payload) //here action.payload has the right type Credentials
			.pipe(
				map(response => this.action.loginSuccess({
				   ...
				})),
				catchError((error: AppError) => of(this.action.loginFail(error)))
			)
		)
	);

// auth.reducer.ts

import * as AuthActions from'./auth.action';
import * as searchActions from'...../search.action';
import * as otherActionsfrom'....../other.action';

type reducerActions = AuthActions.All | searchActions.All  | otherAction.All;

export function authReducer(state: MyState, action: reducerActions) {
  switch (action.type) {
    case reducerActions.LOGIN:
    //here action.payload has the right type Credentials
      return ...........

    case reducerActions.SEARCH:
      return ...........

.....
    default:
      return state;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants