1
type Status = 'Active' | 'Pending' | 'Failed';

type User = {
  name: string;
  age: number;
  status: 'active' // how to get autocomplete, it's lowercase??
} | {
  error: string;
  id: string;
  status: 'failed' // can it be something like Status['Failed']
} |
{
  id: string;
  status: 'pending'
}

Given Status as string union type, sometimes there is need to use the type in another complex object type. Different field structure for each status values.

How to make sure type safety when creating the User with status field?

I would like to reuse same type, something like below

const fooBarFn(status: Status) {
  if (status === 'Pending') {
    return {
      id: '123',
      status
    }
  }
  // rest of fn
}

Are there any other patterns or best practices for the above?

some solution

type RecordX<K extends keyof any> = {
  [P in K]: P;
};

type Status = 'Active' | 'Pending' | 'Failed';

type User<T extends RecordX<Status>> = {   // how to default assign
  name: string;
  age: number;
  status: T['Active']
} | {
    error: string;
    id: string;
    status: T['Failed']
  } |
{
  status: T['Pending']
}

const user: User = {   // need the template type parameter
  name: 'foobar';
  age: 99,
  status: 'NotWorking'  // not working
}

How to default the first generic parameter, so that user doesn't need to provide the type

12
  • status: Status? Commented Jul 12, 2022 at 13:44
  • @depperm but how to create conditional user object with specific structure for each status type Commented Jul 12, 2022 at 13:46
  • What's the problem? if you try to create a User, it will give you type completion if you have typed the other properties. See this example Commented Jul 12, 2022 at 13:57
  • @JuanMendes, i would like to reuse Status Type in other places also, so that why wanted to use same Status inside user type Commented Jul 12, 2022 at 14:06
  • 1
    Ugh, I think I misunderstood your question entirely. Let me try again. Does this meet your needs? If so I can write up that answer. If not, could you show some small code example where it doesn't work for you? Commented Jul 12, 2022 at 15:12

3 Answers 3

1

One possible approach is to define Status in terms of User, instead of doing it the other way around. That way you don't have to repeat yourself very much, and you don't have to try to get the compiler to autocomplete the individual statuses. It looks like this:

type User = {
    name: string;
    age: number;
    status: 'Active'
} | {
    error: string;
    id: string;
    status: 'Failed'
} | {
    id: string;
    status: 'Pending'
}

type Status = User['status'];
// type Status = "Active" | "Failed" | "Pending"

By indexing into the discriminanted union type User with the "status" key, we get the desired union type for Status.

Playground link to code

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

Comments

1

You can achieve this by using enum keyword like this:

enum Status {
  ACTIVE = 'ACTIVE',
  PENDING = 'PENDING',
  FAILED = 'FAILED',
}

type User = {
  name: string;
  age: number;
  status: Status.ACTIVE;
} | {
  error: string;
  id: string;
  status: Status.FAILED;
} |
{
  id: string;
  status: Status.PENDING;
}

Later on you can do this to check the User typing.

const user: User = { ... }

if (user.status === Status.PENDING) {
  // Do something if status is pending
}

Comments

0

First converted literal Status to object format

type Intermediate = {
  Active: 'Active';
  Pending: 'Pending';
  Failed: 'Failed';
}

Then assigned as generic type for Status

type RecordX<K extends keyof any> = {
  [P in K]: P;
};

type Status = 'Active' | 'Pending' | 'Failed';

type User<T extends RecordX<Status> = RecordX<Status>> = {
  name: string;
  age: number;
  status: T['Active']
} | {
  error: string;
  id: string;
  status: T['Failed']
} |
{
  status: T['Pending']
}

const user: User = {
  name: 'foobar';
  age: 99,
  status: 'Active' // autocomplete works
}

5 Comments

You don't seem to be using Intermediate for anything... what does it do?
Why are you using generic here, do you plan to write User<XXX> for some other XXX? Or do you just want it to be RecordX<Status>? If so, why not make User a non-generic type and hardcode T to RecordX<Status>?
@jcalz, how to hardcode T to RecordX<Status> , didn't quite got ur answer. can you post an example.
Here is an example where I successively simplify what you're doing. First I remove the generic and hardcode T as RecordX<Status>. Then I remove RecordX and hardcode T as the definition for RecordX. Then I note that T[K] is equivalent to K, so you can replace T['Active'] with 'Active', etc. Eventually I end up with the discriminated union type from my answer.
@v3 look simple

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.