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
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">'.
/>