1

I have the following object:

const object = {
  root: {
    path: 'path1',
  },
  paths: [
    {
      path: 'path2',
    },
    {
      path: 'path3',
    }
  ],
};

and I would like to build a union type that would look like the following:

type extractedPaths = 'path1' | 'path2' | 'path3';

Is there a way to achieve this with TypeScript?

3
  • 1
    Does this approach meet your needs? Note that your definition of object needs to be changed (e.g., using a const assertion) in order for the compiler to keep track of those strings, otherwise they're all just string. And also note that const extractedPaths = 'path1' | 'path2' | 'path3' is not a type but a value and a weird value at that. Presumably you meant type ExtractedPaths = ... instead. Let me know if this addresses your question and I can maybe write up an answer; otherwise, what am I missing? Commented Apr 23, 2022 at 18:52
  • So, are you likely to accept an answer with that approach or am I missing some requirement you care about? Commented Apr 26, 2022 at 19:19
  • @jcalz Sure I'm happy to accept your answer. Commented Apr 26, 2022 at 20:27

2 Answers 2

1

First, you will need to change your initialization of object so that the compiler knows it's supposed to care about the literal types of the string-valued properties. Otherwise you'll get a value of type {root:{path:string},paths:{path:string}[]} and then no matter what you do you'll get just string out. So let's use a const assertion to get a more specific type for object:

const object = {
  root: {
    path: 'path1',
  },
  paths: [
    {
      path: 'path2',
    },
    {
      path: 'path3',
    }
  ],
} as const;

That gives us the following type:

/* const object: {
    readonly root: { readonly path: "path1"; };
    readonly paths: readonly [
      { readonly path: "path2"; }, 
      { readonly path: "path3"; }
    ]; 
  } */

Great, the compiler knows about "path1", "path2", and "path3".


Then you will need to determine how to identify the types you care about. One possibility is to recursively descend through the type of object and build up a union of any string literal property value types you find along the way. Here's a recursive conditional type that does this:

type DeepStringLiteralValues<T> =
  T extends string ? string extends T ? never : T :
  T extends readonly any[] ? DeepStringLiteralValues<T[number]> :
  T extends object ? { [K in keyof T]-?: DeepStringLiteralValues<T[K]> }[keyof T] : never;

For any type T, if T is itself a string literal type, then DeepStringLiteralValues<T> will just be T. Otherwise, if it's an arraylike type, we evaluate DeepStringLiteralValues<T[number]> for each element type of the array. Otherwise, if it's an objectlike type, we gather the union of DeepStringLiteralValues<T[K]> for every key K in keyof T. Otherwise, it's not a string or an array or an object, so we ignore it and return never. Let's see what that does:

type ExtractedPaths = DeepStringLiteralValues<typeof object>
// type ExtractedPaths = "path1" | "path2" | "path3"

Looks good.


It's also conceivable that your intent is to just get a union of all recursive properties with the key named "path". If so you can do this instead:

type DeepPropKey<T, K extends PropertyKey> =
  K extends keyof T ? T[K] :
  T extends readonly any[] ? DeepPropKey<T[number], K> :
  T extends object ? { [P in keyof T]-?: DeepPropKey<T[P], K> }[keyof T] :
  never;

It's similar to the other version, but DeepPropKey<T, K> is trying to find property value types for properties with key K, instead of grabbing any string literal it can find. For your example, it produces the same output, though:

type ExtractedPaths = DeepPropKey<typeof object, "path">
// type ExtractedPaths = "path1" | "path2" | "path3"

Playground link to code

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

Comments

0

Not sure what you want to achieve here but for example you can have something like (check typescript playground)

type ExtractedPath = 'path1' | 'path2' | 'path3'
type ExtractedObjectPath = {
    path: ExtractedPath

}
type YourObject = {
    root: {
        path: ExtractedPath
    },
    paths: ExtractedObjectPath[]
}

const myObject: YourObject = {
  root: {
    path: 'path1',
  },
  paths: [
    {
      path: 'path2',
    },
    {
      path: 'path3',
    }
  ],
};

2 Comments

Yes this is a potential solution but you have to define the potential values in advance.
I don't understand your problem, the whole point of having type is you have to define it in advance. Can you explain it more?

Your Answer

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