Search code examples
typescripttypescript-generics

Generic types with dynamic mandatory key of specific type


Edit: Solution below https://stackoverflow.com/a/78438593/19831401

I have a Generic typescript type that accepts the key name for text and value as a generic parameter: type Options<T, TKey, VKey> where TKey is the key for the text, VKey the key for the value.

/**
 * Here are my types
 */
type Option<
  T,
  TKey extends PropertyKey,
  VKey extends PropertyKey
> = { [key: PropertyKey]: unknown } & { [K in TKey]: string } & { [K in VKey]: T };

type Options<
  T = unknown,
  TKey extends PropertyKey = "text",
  VKey extends PropertyKey = "value"
> = Array<Option<T, TKey, VKey>>;


/**
 * Question: How can I make a Typescript interface that infer properly TKey and VKey from Options<T, TKey, VKey> properly based on the value passed to
 * textProperty and valueProperty?
 */

// The interface
interface MySelectProps<T> {
  value: T;
  textProperty: PropertyKey; // Defaults to 'text'
  valueProperty: PropertyKey; // Defaults to 'value'
  options: Options<T, MySelectProps<T>['textProperty'], MySelectProps<T>['valueProperty']>;
}

// Exemples
const myProps: MySelectProps<string> = {
  value: "1",
  textProperty: "label",
  valueProperty: "value",
  options: [/* ... */] // Should expect Options<string, "label", "value">
}

const myProps2: MySelectProps<string> = {
  value: "1",
  textProperty: "myTextProperty",
  valueProperty: "myValueProperty",
  options: [/* ... */] // Should expect Options<string, "myTextProperty", "myValueProperty">
}

I'm unable to write an interface type that will infer properly Options<T, TKey, VKey> properly depending on the values passed to textProperty and valueProperty.

Exemple

const myProps2: MySelectProps<string> = {
  value: "1",
  textProperty: "myTextProperty",
  valueProperty: "myValueProperty",
  options: [/* ... */] // Should expect Options<string, "myTextProperty", "myValueProperty">
}

Because i'm passing

textProperty: "myTextProperty",
valueProperty: "myValueProperty",

expected options type is supposed to be of type Options<T, "myTextProperty", "myValueProperty">

Here is a typescript playground for easy testing Playground


Solution

  • It appeared that it wasn't possible to do what I want in Typescript.

    However, by trying to do the opposite: validating the key presence on the type options dynamically I was able to get the exact result I wanted to.

    Opposite way

    Defining TKey and VKey as generic type on MySelectProps was problematic for me because I couldn't define those value based on textProperty and valueProperty.

    As a workaround, setting generic type to TKey and VKey when I use the interface allows me to validate that textProperty and valueProperty is defined on options. It required to force cast textProperty to TKey and valueProperty to VKey.

    export interface SvSelectProps<T, TKey extends string, VKey extends string> {
      value: T;
      textProperty?: TKey,
      valueProperty?: VKey,
      options: Options<T, TKey, VKey>;
    }
    
    /**
     * Svelte component demo
     */
    <script lang="ts" generics="T, TKey extends string, VKey extends string">
      let {
        options,
        value,
        textProperty = "text" as TKey, // Forcing the cast sets the value of TKey to textProperty
        valueProperty = "value" as VKey,  // Forcing the cast sets the value of VKey to valueProperty
      }: SvSelectProps<T, TKey, VKey> = $props();
    </script>
    

    Usage

    // SomePage.svelte
    <script lang="ts">
      import MySelect, { type MySelectProps } from "$lib";
    
      let options: Options<string, "text", "value"> = [
        { value: "1", text: "One" },
        { value: "2", text: "Two" },
        { value: "3", text: "Three" },
      ];
    </script>
    
    <SvSelect
      value="1"
      textProperty="texttypo" // TS: Type '"texttypo"' is not assignable to type '"text"'.ts(2322)
      valueProperty="value" 
      options={options} />
    

    Final solution

    It seems that passing generic type to TKey and VKey and force casting them made possible to do exactly what I wanted to do. It requires to not type options because when it's typed, typescript warns about the textProperty/valueProperty being not present in options.

    // SomePage.svelte
    <script lang="ts">
      import MySelect, { type MySelectProps } from "$lib";
    
      // See: untyped
      let options = [
        { value: "1", text: "One" },
        { value: "2", text: "Two" },
        { value: "3", text: "Three" },
      ];
    </script>
    
    <SvSelect
      value="1"
      textProperty="texttypo" 
      valueProperty="value" 
      options={options} // Type '{ value: string; text: string; }[]' is not assignable to type 'Options<string, "texttypo", "value">'.
     />