43

I'm creating a generic class named Loadable<T>, with two fields. One is named state, and contains the current state of the loadable resource. The second is named data, and contains the value of the loadable resource.

state is defined as "loading" | "loaded" | "error", and I'd like to type of data to change based on the current value of state.

For example, if state is "loading", then data should be null. I cannot figure out the syntax to do this.

I've created a type named LoadableState<T, U> which is defined as:

    type LoadableState<T, U> =
        T extends "loaded" ? U :
        T extends "loading" ? null :
        T extends "error" ? string :
        never;

Where T passed must be the type "loaded" | "loading" | "error".

This part works, but trying to define data from this does not.

export class Loadable<T> {
    public state: "loaded" | "loading" | "error" = "loading";

    public data: LoadableState<state, T>;
    //                         ^^^^^ Cannot find name 'state`
}

TypeScript throws the following error: Cannot find name 'state'


Other things I've tried:

public data: LoadableState<this.state, T>;
// Duplicate identified 'state'

public data: LoadableState<typeof this.state, T>;
// Cannot find name 'this'

public data: LoadableState<typeof state, T>;
// Cannot find name 'state'

public data: state extends "loaded" ? T :
    state extends "loading" ? null :
    state extends "error" ? string :
    never;
// Cannot find name 'state'

I'm not sure if this is actual possible. If this isn't, is there another solution to this problem?

1

2 Answers 2

66

I don't think there's a way (or at least not a clean way) to have the type of one class property to depend on the current value of another property.

You're better off combining both these properties into a single object:

type LoadState<T> = {
    state: "loading",
} | {
    state: "loaded",
    data: T
} | {
    state: "error",
    data: string; // or maybe 'message'
}

export class Loadable<T> {
    state: LoadState<T> = { state: "loading" }
}

Though the class wrapper isn't actually very useful here, you might consider not using the class at all.

When using this type, Typescript will enforce that you check the state before trying to access data:

function handleLoadState<T>(loadState: LoadState<T>) {
   loadState.data // ERROR, can't access data, since it might not exist
   if(loadState.state === "loaded")
       loadState.data // okay
   }
}

This pattern is called a Discriminated Union and is quite commonly used in Typescript.

Sign up to request clarification or add additional context in comments.

1 Comment

This is a great solution to solve this problem. Thank you!
0

here is a better way

public data: LoadableState<state, T>;
//                         ^^^^^ Cannot find name 'state`

pass "loaded instead if state" for Cannot find name state and then add type boundry for every state

if (state extends 'loaded') {
   // your code
}

<LoadableComponent<//put state here staticly> />

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.