简体   繁体   中英

How to set the type of property based on the parameter passed in typescript?

I am creating a react app and also using typescript. I want to have a globalState which will have several properties. I also need to use useReducer . I create an interface for action of the reducer function. The interface takes in one parameter N which will be name of action. Now I want to set the type of data according to the N parameter passed.

I have almost created it using ternary operators.

export interface IGlobalState {
   loading: boolean;
   userData: IUserData;
}
interface IUserData {
   loggedIn: boolean;
   name: string;
}

type GlobalStateActionNames = "setLoading" | "setUserData"

export interface IGlobalStateAction<N extends GlobalStateActionNames = GlobalStateActionNames> {
   data: N extends "setLoading"
      ? boolean
      : N extends "setUserData"
      ? IUserData
      : any;
   name: N;
}

export const GlobalStateReducer = (
   state: IGlobalState,
   {name, data}: IGlobalStateAction
): IGlobalState => {

    switch(name){
        case "setLoading": return {...state, loading: data};
        default: return state;
    }
}; 

There is only one problem. The data parameter in the reducer will extends all the types which could be the possible value of data. So when I set loading to data it gives error.

Type 'boolean | IUserData' 'boolean | IUserData' is not assignable to type 'boolean'

I have two questions.

  • How could I resolve this error?
  • Is there any better to achieve the above stated functionality?

Note: I know I can solve this by using case "setLoading": return {...state, loading: data as boolean}; . But globalState will have many other props. So I don't want to that for all the values.

The issue is your IGlobalStateAction , to use it as You need you would need to pass N generic inside, as this is only way to have conditional working. As you cannot determine which action is passed to reducer, N cannot be passed at this level. You can fix that by type guarding this type or changing it to standard union, its maybe less generic, but works without any conditional typyings.

Using union types instead of conditional

// pay attention, type is now simple union
export type IGlobalStateAction = 
  | {
    name: "setLoading",
    data: boolean
  }
  | {
    name: 'setUserData',
    data: IUserData
  }

export const GlobalStateReducer = (
   state: IGlobalState,
   action: IGlobalStateAction
): IGlobalState => {

    switch(action.name){
        case "setLoading": return {...state, loading: action.data}; // correctly infers bool
        default: return state;
    }
}; 


Using conditional type with type guard

The second solution is, if you want to leave your conditional type - having type guard. Consider:

// here your original type

// type guard narrowing the type
const isActionWithName = <N extends GlobalStateActionNames>
(a: IGlobalStateAction, n: N): a is IGlobalStateAction<N> => a.name === n;

export const GlobalStateReducer = (
   state: IGlobalState,
   action: IGlobalStateAction
): IGlobalState => {
  // using type guard
  if (isActionWithName(action, 'setLoading')) {
    return {...state, loading: action.data};    
  }
  return state;
}; 

What is happening here - isActionWithName function checks the name and puts correct generic into IGlobalStateAction (this can be spotted here - a is IGlobalStateAction<N> ). At the point of check, we can determine which exactly name it is, and how it impacts the data type.

Sorry for how it looks I was doing it from the phone, but you can check out my repo I have there what you ir looking for

https://github.com/EnetoJara/-eneto-react-init


export const DO_SOME = ”DO_SOME”;
export type DO_SOME  = typeof DO_SOME;

interface AppAction<T,P> {

   type: T;
   payload?: P;

}

export function actionCreator(someParam; ParamType): AppAction<DO_SOME, ParamType> {

  return {
     type: DO_SOME,
     payload: someParam
}


Also remember you can do something like this on them functions


function nameOfFunction<T, R> (param: T): Promise<R> {

   return httpCal(whatever).then((res: R) => red);

T = type R = result Genetics that's how they are call

Then just to use it let's imagine

const a = nameOfFunction<UserModel, HttpCode> (user).then((res: HttpCode)=>res);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM