4

I have this type where my value property is "optional" (if T is not undefined)

type AsyncState<T = undefined> = {
    value?: T;
    loading: boolean;
    error: { reason: string } | null;
}

Now I need to somehow create new object that depends on AsyncState parameter - add value property if T is not undefined and don't if T is undefined. (this is just dummy example of more complicated logic, but since types are problem, it should be enough)

function asyncGet<T>(initialState: AsyncState<T>) {
    return typeof initialState.value !== 'undefined' 
        ? (s: AsyncState<T>) => ({ ...initialState })
        : (s: AsyncState) => ({ loading: initialState.loading, error: initialState.error });
}


const first: AsyncState<string> = {
    loading: true,
    error: null,
    value: ""
}

const second: AsyncState<string> = {
    loading: true,
    error: null,
    value: ""
}

const creator = asyncGet(first);

/* 
Argument of type 'AsyncState<string>' is not assignable to parameter of type 'AsyncState<string> & AsyncState<undefined>'.
  Type 'AsyncState<string>' is not assignable to type 'AsyncState<undefined>'.
    Type 'string' is not assignable to type 'undefined'.
*/
creator(second);

Here is typescript playground.

4
  • 1
    Why are you using a conditional type instead of type AsyncState<T> = {loading: boolean; error: {reason: string} | null; value?: T | null}? The difference seems small enough and it would have to be easier to deal with... Commented Sep 9, 2019 at 19:32
  • @jcalz I had similar setup, but then in my redux store I'd always have that value: null although I don't even need it there (basically I just need to track error and loading for specific properties, e.g. login action) Commented Sep 9, 2019 at 19:34
  • 1
    If value is optional as in {value?: T | null} then you don't need value: null, right? It would just be left out. I could possibly take as given that you need to use the relatively clunky conditional type, but I'd like to see a minimal reproducible example here with actual use cases. As it stands I don't how to proceed (what's combineReducers()? This question is not tagged redux and if you need redux expertise then it probably should be... if it's not about redux then make some example use cases without being dependent on it). Good luck! Commented Sep 9, 2019 at 19:39
  • @jcalz Please take a look at edited solution (one using value?: T), and also without any redux references. Commented Sep 9, 2019 at 19:59

1 Answer 1

1

You can resolve this by making the returned function that needs to infer the actual type of T generic.

function asyncGet<T>(initialState: AsyncState<T>) {
  return typeof initialState.value !== "undefined"
    ? (s: AsyncState<T>) => ({ ...initialState })
    : <U>(s: AsyncState<U>) => ({
        loading: initialState.loading,
        error: initialState.error
      });
}

That said, this will get you in trouble if you ever try to override TypeScript's inference by calling it like this: asyncGet<string>({ loading: true, error: null })

A better solution is to use a conditional type to specify that the function conditionally uses the inferred value of the call to the returned function.

function asyncGet<T>(initialState: AsyncState<T>): 
  <U>(s: AsyncState<[T] extends [undefined] ? U : T>) => 
    AsyncState<[T] extends [undefined] ? U : T> {

  return typeof initialState.value !== "undefined"
    ? (s) => ({ ...initialState })
    : (s) => ({
        loading: initialState.loading,
        error: initialState.error
      });
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.