1

I'm trying to create a function that safely looks up the property in deeply nested object (theme in this case). I should mention that I don't want to statically define its type because they are intended to be a subject to frequent change and thus use typeof

This functions accepts the object itself and 3 keys, but the problem is — I can't correctly infer types for all of them.

const theme = {
    button: { margin: { sm: "sm" } },
    form: { padding: { lg: "lg" } }
}

type Theme = typeof theme

type CompoName = keyof Theme;

// styles of component with give key
// { margin: ..., padding: ... }
type CompoStyle<C> = C extends CompoName ? keyof Theme[C] : never;

// string 
// S = margin, padding
type CompoStyleMod<C, S> = S extends keyof CompoStyle<C>
  ? CompoStyle<C>[S]
  : never;

const getStyle = (
    t: Theme,
    name: CompoName,
    style: CompoStyle<typeof name>,
    mod: CompoStyleMod<CompoName, typeof style>
) => {
    return t[name][style][mod]
}

The result of this in TypeScript 3.6.3:

Element implicitly has an 'any' type because expression of type '"margin" | "padding"' can't be used to index type '{ margin: { sm: string; }; } | { padding: { lg: string; }; }'.

  Property 'margin' does not exist on type '{ margin: { sm: string; }; } | { padding: { lg: string; }; }'.

Looks like it is not possible to look up union types with union types and some kind of inference required midway.

Any ideas?

  • 2
    Does [this](https://www.typescriptlang.org/play/#code/MYewdgzgLgBFAWBTAtomBeGBvAsAKBkJgCMBXKKcALmxmQEMAnAcwEswasYJkaAiHnxgBfEQBp8RGADMQjXrQAO9ACYr2zTjAA2mmH11DRw-Cbz4oAT0VoAKklQY41xCGlwHifPlCRYzRCgAZSttNEwAHkkiAGkYRAAPKEQwFQgYAGtESzcYexRECQIiIPiklLTM7Nz81ABtGIBdIqkAWTLk1PSsnPdaxAbGuqDG-AA+AAooGn6xGDB6VBoYuehLMJogueQQFRpWgEoMMbg6hdQhtbChnZVGgG5vPF9obmQnAODQxCnPOb4yBRwHx-gwWOwQfpBAd7jAAPRw7hQRgaHzgV66D6BELrH4IAr-WTySF8ZRqDQkwww+GI6AosDMIA) work for you? I'm not sure why you've got those conditional types in there but my inclination would be to avoid them and go with generic lookups. – jcalz Oct 18 '19 at 15:12
  • Oh, this looks much clearer then I expected! Ready to take this as correct answer. Thank you! – walkthroughthecode Oct 18 '19 at 15:18

1 Answers1

4

I'd be inclined to avoid all conditional types, since the compiler can't reason about them very well and it doesn't look like you need them. Instead of type Foo<T> = T extends U ? F<T> : never, you could just constrain T, like type Foo<T extends U> = Foo<T>, which is more straightforward for the compiler.

The solution here is probably to make getStyle() generic in enough type parameters that the compiler understands that each parameter is drilling down into an object and looking up its properties. Like this:

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];

Here we say that t is of type Theme, that name is of some generic type K constrained to keyof Theme, that style is of some generic type S constrained to keyof Theme[K], and that mod is of some generic type M constrained to keyof Theme[K][S]. That allows t[name][style][mod] to compile with no error, and the return type of getStyle() is inferred to be Theme[K][S][M], meaning the output will be fairly strongly typed also:

const sm = getStyle(theme, "button", "margin", "sm"); // string
const lg = getStyle(theme, "form", "padding", "lg"); // string

Okay, hope that helps. Good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360