7

Consider the following interface:

interface Theme {
  color: {
    primary: {
      light: string
      base: string
      dark: string
    }
    secondary: {
      lighter: string
      light: string
      base: string
      dark: string
      darker: string
    }
  }
}

I'm trying to write a type that will allow a tuple, the first element mapped to any key in colors, and the second mapped to any key under that (ie: base).

ie:

['primary', 'light'] ✅ valid
['secondary', 'darker'] ✅ valid
['primary', 'darker'] 🛑 invalid

Here is an attempt i've made on tsplayground the problem i'm facing here is that if i want to allow multiple keys to be passed as the first arg, then second needs to satisfy all of the firsts. Is there a way to tell typescript to use the literal value passed as the type?

type PickThemeColor<C extends keyof Theme['color'] = keyof Theme['color']> = [
  C,
  keyof Theme['color'][C]
]

// 👇🏼 this complains because 'darker' doesnt appear in both 'primary' and 'secondary' keys

const x: PickThemeColor<'primary' | 'secondary'> = ['secondary', 'darker']

2 Answers 2

3

You have here 2 options, a generic, which you need to specify unfortunately or a union:

// A generic way
type Typle<K extends keyof Theme['color']> = [K, keyof Theme['color'][K]];

const test1: Typle<'primary'> = ['primary', 'light'];
const test2: Typle<'secondary'> = ['secondary', 'darker'];
const test3: Typle<'primary'> = ['primary', 'darker']; // fails

// A union way.
type Typle2 <K = keyof Theme['color']> = K extends keyof Theme['color'] ? [K, keyof Theme['color'][K]] : never;

const test4: Typle2 = ['primary', 'light'];
const test5: Typle2 = ['secondary', 'darker'];
const test6: Typle2 = ['primary', 'darker']; // fails

Otherwise you need a creation function to avoid the required generic value.

// a helper function way.
const craeteType = <K extends keyof Theme['color']>(v: Typle<K>): Typle<K> => {
  return v;
}

const test7 = craeteType(['primary', 'light']);
const test8 = craeteType(['secondary', 'darker']);
const test9 = craeteType(['primary', 'darker']); // fails

Playground

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

2 Comments

Thanks! one thing, my intellisense is suggesting all possible values for the second argument instead of restricting them to keys of the first, is there a way around this? 🤔
Ah, then I think we need to remove = keyof Theme['color'] and always to set it in : Typle<>. I've updated the answer.
2

Actually you were very close. The only missing thing was distributing color key:

type ColorKey = keyof Theme['color'];
type ShadeKey<K extends ColorKey> = keyof Theme['color'][K];

type PickThemeColor<C extends ColorKey> = C extends ColorKey ? [C, ShadeKey<C>] : never;

const x1: PickThemeColor<'primary' | 'secondary'> = ['primary', 'light'] // OK
const x2: PickThemeColor<'primary' | 'secondary'> = ['secondary', 'darker'] // OK
const x3: PickThemeColor<'primary' | 'secondary'> = ['primary', 'darker'] // Error

Playground


ColorKey and ShadeKey where extracted just to simplify PickThemeColor (nothing new here). What makes the difference is C extends ColorKey part as it distributes over union of color keys.

So PickThemeColor<'primary'> will produce
["primary", "light" | "base" | "dark"]

And PickThemeColor<'primary' | 'secondary'> will produce
["primary", ShadeKey<"primary">] | ["secondary", ShadeKey<"secondary">]

2 Comments

Thanks! this looks great. Im still seeing the same as above where my intellisense for the second tuple value always suggests all possible values (instead of valid ones based on the first argument) - is this possible?
Not sure regarding intellisense, but type checking works as expected as you can see

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.