Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript type to extract all values in a nested record

Tags:

typescript

Suppose I have:

interface JaggedRecord {
    [k: string]: string | JaggedRecord;
}

const MyKeys = {
    SubKey1: {
        child1: 'child1',
        child2: 'child2',
    },
    SubKey2: {
        child3: 'child3',
        child4: 'child4',
        SubKey3: {
            child5: 'child5',
        },
    },
    child6: 'child6',
} as const;

// obviously order doesn't matter
type AllValues = 'child1' | 'child2' | 'child3' | 'child4' | 'child5' | 'child6';

Now I want to create a type which dynamically generates AllValues.

Ever the effective rubber duck, Stack Overflow forced me to figure out the answer in order to write the question.

like image 533
dx_over_dt Avatar asked Feb 05 '26 04:02

dx_over_dt


2 Answers

Here you have a bit simplified solution:

const MyKeys = {
  SubKey1: {
    child1: 'child1',
    child2: 'child2',
  },
  SubKey2: {
    child3: 'child3',
    child4: 'child4',
    SubKey3: {
      child5: 'child5',
    },
  },
  child6: 'child6',
} as const;

type All<T> = {
  [P in keyof T]: T[P] extends string ? P : All<T[P]>
}[keyof T]

type Result = All<typeof MyKeys> // 'child1' | 'child2' | 'child3' | 'child4' | 'child5' | 'child6';
like image 162
captain-yossarian Avatar answered Feb 08 '26 00:02

captain-yossarian


The trick, of course, turned out to be abstraction that made it easier to reason about what I was retrieving.

type JaggedRecordKeys<T extends JaggedRecord> = {
    [K in keyof T]: T[K] extends JaggedRecord ? [K, T[K]] : never;
}[keyof T];

type ExtractAllValues<T extends JaggedRecord> = 
    | { [K in keyof T]: T[K] extends string ? T[K] : never }[keyof T] & string
    | { [K in JaggedRecordKeys<T>[0]]: ExtractAllValues<Extract<JaggedRecordKeys<T>, [K, any]>[1]>; }[JaggedRecordKeys<T>[0]];

type AllValues = ExtractAllValues<typeof MyKeys>;

First we create a type (JaggedRecordKeys) that takes the keys of T whose value is not a string, then extracts a union of tuples of that key and that key's value.

Then, we make ExtractAllValues return all the keys of T whose value is a string unioned with the recursive call of every extracted value of T's keys whose value is a JaggedRecord, and dereference that recursive call's return value.

I suspect I could make JaggedRecord generic to allow any type of child, but when I tried it, ExtractAllValues said it had a circular reference. Since my use-case is specifically for strings, I left it as-is.

like image 38
dx_over_dt Avatar answered Feb 07 '26 23:02

dx_over_dt



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!