7

So from:

export interface Category{
  val: string;
  icon: string
}
const categoryArray: Category[] = [
  {
    val: 'business',
    icon: 'store'
  },
  {
    val: 'media',
    icon: 'video'
  },
  {
    val: 'people',
    icon: 'account'
  },

  ... 

I'd like to get a Union type back like this:

'business' | 'media' | 'people' ... 

I don't know what kind of syntax or helpers there are for this, maybe none at all. I realise this way might be backwards, and should perhaps use an Enum, but before that, I want to know it it's possible.

Some fictional examples of what I'd like to do, but the solution I expect to be more complex

type Cats = keysof[] categoryArray 'val'  
type Cats = valuesof categoryArray 'val'

The following is close, but returns string:

export type CatsValType = typeof categories[number]['val']

Or the following; instead of the types I need the string literals

type ValueOf<T> = T[keyof T];
type KeyTypes = ValueOf<typeof categories[number]> // Returns: `string`

There are similar questions like: Is there a `valueof` similar to `keyof` in TypeScript? but they don't assume an array of objects.

And the example here: https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html is similar, but I don't want to return the type, but the value of the fields, so I get a Union type back.

TrySpace
  • 2,233
  • 8
  • 35
  • 62

1 Answers1

7

You can do it if:

  • The values in the array don't change at runtime (since type information is a compile-time-only thing with TypeScript); and

  • You tell TypeScript that they values won't change by using as const; and

  1. You don't give the categoryArray constant the type Category[], because if you do the result would just be string (because Category["val"]'s type is string) rather than the string literal union type you want.

Here's an example (playground link):

export interface Category{
  val: string;
  icon: string
}
const categoryArray = [
  {
    val: 'business',
    icon: 'store'
  },
  {
    val: 'media',
    icon: 'video'
  },
  {
    val: 'people',
    icon: 'account'
  },
] as const;

type TheValueUnion = (typeof categoryArray)[number]["val"];
//   ^? −− "business" | "media" | "people"

The key bits there are the as const and type TheValueUnion = (typeof categoryArray)[number]["val"];, which breaks down like this:

  1. typeof categoryArray gets the type of categoryArray (the inferred type, since we didn't assign a specific one).
  2. [number] to access the union of types indexed by number on the type of categoryArray.
  3. ["val"] to access the union of types for the val property on the union from #2, which is the string literal type you want: "business" | "media" | "people".
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks, that almost worked for me, now the problem is that any 'optional' property on the `Category` becomes readonly and mandatory. I didn't include any optional in the example, but let's say I have a `disabled?: boolean;` there, it will error: `Property 'disabled' does not exist on type '{ readonly val: "art"; readonly... etc.` – TrySpace Jan 14 '22 at 16:14
  • @TrySpace - That's odd, [I don't get that](https://tsplay.dev/wR9Y1W) and I'm not sure where that error would come up, we're not doing anything to `Category` above. Can you update that playground example to demonstrate the problem? – T.J. Crowder Jan 14 '22 at 16:39
  • https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgYU1QHNoBPOAbwCg44A3TAGwC44BnGKZYgblsTYISNp25I+AgCYJ2mAEZNgUgPxt5ECEsxJ+AX2pCknONiLBSUMgEEoUTBQC8cANoCadOo1ZwA5PIBXdmRgdnZfABoBOgQjNl9OaGBI6LgZOUVlNi4A4AE9KLoPT294gFtlBEwUz0FhePoEKWAIGs98wqpU0r8wFrAlNpi4vxwhAJRfDuoAXThMdlNhTn5qGDI+uAAVAAtgADVmXIBVJARhOGcACnW+iHRTc0sbOwcAShckALL5NBmXABE3gBM34AHowZ4AHoqaiGZZaYAAOiYEGIVzMJHItnsZCR6QUSikbyAA – TrySpace Jan 15 '22 at 17:48
  • @TrySpace - You're trying to use `disabled` on the **array**. It's not on the array, it's on array **elements**. – T.J. Crowder Jan 16 '22 at 12:28
  • You're right, This is what I meant: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgYU1QHNoBPOAbwCg44A3TAGwC44BnGKZYgblsTYISNp25I+AgCYJ2mAEZNgUgPxt5ECEsxJ+AX2pCknONiLBSUMgEEoUTBQC8cANoCadOo1ZwA5PIBXdmRgdnZfABoBOgQjNl9OaGBI6LgZOUVlNi4A4AE9KLoPT294gFtlBEwUz0FhePoEKWAIGs98wqpU0r8wFrAlNpi4vxwhAJRfDuoAXThMdlNhTn5qGDI+uAAVAAtgADVmXIBVJARhOGcACnW+iHRTc0sbOwcAShckALL5NBmXABE3gBM34AHowZ4AHoqaiGZZaYAAOiYEGIVzMJHItnsZBcAEYZkj0golFI3kA I cannot conditionally use the `disabled` property, unless it is certainly defined in the read-only object. – TrySpace Jan 17 '22 at 13:50
  • While technically the `disabled` prop doesn't exists, I need to conditionally check the presence of the `disabled` prop. As if I had `categoryArray: Category[] = ...` defined, but I can't use both that AND TheValueUnion... – TrySpace Jan 17 '22 at 13:52
  • @TrySpace - You could always do something like [this](https://tsplay.dev/WPxEeW). :-) (You could even do a runtime validation check to make sure the type assertion isn't wrong as the code changes over time.) – T.J. Crowder Jan 17 '22 at 14:03