0

I have the following example code

const routes = [
  { path: '/' },
  { path: '/test' },
  { path: '/new' },
]

And I want the corresponding union type for the possible paths, i.e.

type Path = '/' | '/test' | '/new'

Obviously this does not work:

type Path = typeof routes[number]['path'] // just string

But it would work if I changed routes to be

const routes = [
  { path: '/' } as const,
  { path: '/test' } as const,
  { path: '/new' } as const,
]

Is there a less repetitive way to achieve the same thing? I know I could also make the whole array readonly with

const routes = [
  { path: '/' },
  { path: '/test' },,
  { path: '/new' },
] as const

but I want to know if it is possible without it.

v-moe
  • 1,203
  • 1
  • 8
  • 25
  • 2
    Why is it not an option – ertucode Mar 23 '23 at 09:06
  • It collides with the library that I am using – v-moe Mar 23 '23 at 09:07
  • Which library? Angular? – ertucode Mar 23 '23 at 09:10
  • No, it's not. And it does not really matter, I am interested in the generic solution to this problem, I can obviously work around it by adding "as const" to each element – v-moe Mar 23 '23 at 09:13
  • 1
    @v-moe - So we can avoid triggering the error you're getting from the library while trying to help you, please quote the error and ideally show an example of the code causing it. For instance: `someLibraryFunction(routes)` causes "TS(1234) Error here". – T.J. Crowder Mar 23 '23 at 09:21
  • Sorry, I will edit my question to make it clearer that I am interested in understanding if what I want is even possible, and if yes how (let's say academic interest). I have several workarounds with type assertions to make it work in my concrete situation – v-moe Mar 23 '23 at 09:25
  • 1
    Just asking why is the last not an option? – Tushar Shahi Mar 23 '23 at 09:26

3 Answers3

1

You've said you can't use as const on the array as it conflicts with a library you're using.

If your starting point is:

const routes = [
    { path: '/' },
    { path: '/test' },
    { path: '/new' },
];

you can't get to the Path type you've asked for based on the type of routes, because the type information necessary to do that no longer exists. The type of routes above is just { path: string; }[].

If you want to derive Path from routes, you're going to have to use a const assertion somewhere.

If it's purely a type problem with that library requiring a mutable type when it's not actually going to modify the array, you could work around that by defining the routes as const initially, then getting a reference to that array without the readonly aspect that you can use with the library. There are at least a couple of ways to do that.

The simple one is: If you can get the type that the library expects (which you should be able to, either directly because it exports a type for it, or indirectly by getting it from the parameter of a library function), you can simply do this:

// The read-only version of the routes
const readonlyRoutes = [
    { path: '/' },
    { path: '/test' },
    { path: '/new' },
] as const;

type Path = typeof readonlyRoutes[number]["path"];
//   ^? type Path = "/" | "/test" | "/new"

// Another reference to the same array, but with a mutable type for the library to use
const routes = readonlyRoutes as any as Route[];
//    ^? const routes: Route[]
``

Then assuming a stand-in library function like this:

```lang-typescript
function exampleLibraryFunction(r: {path: string}[]) {
    // ...
}

This call would fail:

exampleLibraryFunction(readonlyRoutes);

but this one works:

exampleLibraryFunction(routes);

Playground link

If they don't export the type, you can get it from a library function like this (assumes the array is the first parameter):

type Route = Parameters<typeof exampleLibraryFunction>[0];

If you want to be more general, you can use the Mutable utility type from this answer. That type looks like this:

type ExpandRecursively<T> = T extends object
    ? T extends (...args: any[]) => any
        ? // Functions should be treated like any other non-object value
          // but will/can identify as an object in JS
          T
        : { [K in keyof T]: ExpandRecursively<T[K]> }
    : T;

type Mutable<T> = ExpandRecursively<{
    -readonly [K in keyof T]: T[K] extends {}
        ? Mutable<T[K]>
        : T[K] extends readonly (infer R)[]
        ? R[]
        : T[K];
}>;

Using it for your example:

// The read-only version of the routes
const readonlyRoutes = [
    { path: '/' },
    { path: '/test' },
    { path: '/new' },
] as const;

type Path = typeof readonlyRoutes[number]["path"];
//   ^? type Path = "/" | "/test" | "/new"

// Another reference to the same array, but with a mutable type for the library to use
const mutableRoutes = routes as Mutable<typeof routes>;
//    ^? const routes: [{ path: "/"; }, { path: "/test"; }, { path: "/new"; }]

Playground link

Again, while those solutions use as const for readonlyRoutes, they only use it for the purposes of creating Path, and it's unavoidable. The actual array you'd use with the library is routes, which isn't typed as read-only.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • This is one of the workarounds I had thought of, but it still uses 'as const' and asserts back to 'Mutable<...>' don't really like it and doesn't answer my question. But I vote it up since it is a working and reasonable workaround – v-moe Mar 23 '23 at 09:33
  • 1
    @v-moe - If your starting point is `const routes = [{ path: "/" }, { path: "/test" }, { path: "/new" }];`, you **can't** get to the `Path` type you want from `routes`, because the type information necessary to do it is already gone. The type of that `routes` is simply `{ path: string; }[]`. I've updated the answer to make that explicit (and to add a simpler option). That means you'll *have* to work around the library's accepting a mutable type. – T.J. Crowder Mar 23 '23 at 09:53
  • @v-moe - So in my view, yes, this answers your question, but of course in the end that's up to you, and in any case there's no reason not to wait a few days to see if someone can prove me wrong. :-) Wouldn't be the first time. Happy coding! – T.J. Crowder Mar 23 '23 at 09:57
  • 1
    Thank you! This is what I was looking for. "It's not possible" is the best result from a scientific point of view after all :D – v-moe Mar 23 '23 at 11:03
0

We can use a helper type from another stackoverflow question to accomplish this. https://stackoverflow.com/a/49670389/18377008

type DeepReadonly<T> =
    T extends (infer R)[] ? DeepReadonlyArray<R> :
    T extends Function ? T :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>;
};
// Defining the routes `as const` and making sure it satisfies the DeepReadonly version of the library routes.
const routes = [
  { path: '/' },
  { path: '/test' },
  { path: '/new' },
] as const satisfies DeepReadonly<LibraryRoutes>

// Getting types as string literals
type Path = typeof routes[number]['path']

// Converting the type of the variable, and we will feed this variable to the library.
const r = routes as unknown as LibraryRoutes
ertucode
  • 560
  • 2
  • 13
-3

Here's what you are looking for:

type routesToUnion = [
  { path: '/' },
  { path: '/test' },
  { path: '/new' },
][number]['path']

//type routesToUnion = "/" | "/test" | "/new"
aluntu
  • 1
  • 1
  • The type is correct, but I don't have the variable anymore. – v-moe Mar 23 '23 at 16:26
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 29 '23 at 00:11