2

I've been playing with Inferring types of deeply nested object in Typescript.

Original code

const theme = {
    button: { margin: { sm: "sm" } },
    form: { padding: { sm: "sm1" } }
} as const;

type Theme = typeof theme;

const getStyle = <
    K extends keyof Theme,
    S extends keyof Theme[K],
    M extends keyof Theme[K][S]
>(t: Theme, name: K, style: S, mod: M) => t[name][style][mod];

getStyle(theme, 'button', 'margin', 'sm');

I have introduced one variation from the original example - in my code leaf nodes always have the same structure {sm: string}.

What I am struggling with is to modify getStyle in such way that the client can specify only 2 levels of keys, and benefit from the fact that the structure of the nodes is always the same;

const getStyleNew = <
    K extends keyof Theme,
    S extends keyof Theme[K]
>(t: Theme, name: K, style: S) => t[name][style].sm;

Unfortunately, this fails with:

Property 'sm' does not exist on type '{ readonly button: { readonly margin: { readonly sm: "sm"; }; }; readonly form: { readonly padding: { readonly sm: "sm"; }; }; }[K][S]'.

Is there a way to convince the compiler that sm is available on t[name][style] in the modified function?

Playground link

Lesiak
  • 22,088
  • 2
  • 41
  • 65

1 Answers1

2

One way to do this is to assign the style record to an object with an sm property that is typed using a conditional Index type to account for possible absence of sm. This will even infer an exact literal type for the result instead of just string. Here's a generic solution, but you could also use Theme instead of T:

type Index<T, K> = K extends keyof T ? T[K] : undefined;

const getStyleNew = <
  T,
  K extends keyof T,
  S extends keyof T[K]
>(t: T, name: K, style: S) => {
  const {sm}: {sm: Index<T[K][S], 'sm'>} = t[name][style]
  return sm
};

const test1 = getStyleNew(theme, 'button', 'margin'); // inferred: test1: "sm"
const test2 = getStyleNew(theme, 'form', 'padding'); // inferred: test2: "sm1"

If you query a styles record without an sm (e.g. header: { fontSize: {} }), the inferred type will be undefined:

const test3 = getStyleNew(theme, 'header', 'fontSize'); // inferred: test3: undefined

Not sure if this is the most elegant solution possible, but the extends clauses guarantee type safety on the keys, so it should be correct.

TypeScript playground

Oblosys
  • 14,468
  • 3
  • 30
  • 38