Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript, map types from object to tuple

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']
like image 473
Samuel Avatar asked Jun 04 '20 10:06

Samuel


2 Answers

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

like image 197
satanTime Avatar answered Oct 17 '22 09:10

satanTime


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

like image 39
Aleksey L. Avatar answered Oct 17 '22 10:10

Aleksey L.