In TypeScript, is it possible to base the union of allowed literals for an object's property on the value entered in another property (essentially referencing this
instance of the type)?
A pseudo example of what I mean (I don't think TMethodKeys
is relevant, but I included it just in case):
type TMethodKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any
? K
: never;
}[keyof T];
// ---------------------
type OptionA = {
aFunctionForOptionA: () => string;
anotherFunctionForOptionA: () => number;
}
type OptionB = {
thisIsAFunctionForOptionB: () => boolean;
thisIsAnotherFunctionForOptionB: () => bigint;
}
type Base = {
baseFunctionA: () => OptionA;
baseFunctionB: () => OptionB;
}
type Objective = {
baseKey: TMethodKeys<Base>;
subKey: TMethodKeys<ReturnType<Base[Objective['baseKey']]>>; // I want the allowed union of literals of this property to be based on the "this" instance of `Objective` it's `baseKey`.
}
const Test = [
{
baseKey: 'baseFunctionA',
subKey: '...' as never, // Because `baseKey` is `baseFunctionA`. This instance of `Objective` should allow for: `'aFunctionForOptionA' | 'anotherFunctionForOptionA'`
},
{
baseKey: 'baseFunctionB',
subKey: '...' as never, // Because `baseKey` is `baseFunctionB`. This instance of `Objective` should allow for: `'thisIsAFunctionForOptionB' | 'thisIsAnotherFunctionForOptionB'`
},
] as const satisfies Objective[];
Similar to how the Event
subtype for the ev
parameter of the listener
callback is based on what is entered on type
:
interface Document {
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
We can use distributive conditionals to create a union of objects, essentially creating a discriminated union around the baseKey
prop.
Essentially the type Objective
becomes:
type Objective = {
baseKey: "baseFunctionA";
subKey: TMethodKeys<OptionA>;
} | {
baseKey: "baseFunctionB";
subKey: TMethodKeys<OptionB>;
}
Final code:
type TMethodKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any
? K
: never;
}[keyof T];
// ---------------------
type OptionA = {
aFunctionForOptionA: () => string;
anotherFunctionForOptionA: () => number;
}
type OptionB = {
thisIsAFunctionForOptionB: () => boolean;
thisIsAnotherFunctionForOptionB: () => bigint;
}
type Base = {
baseFunctionA: () => OptionA;
baseFunctionB: () => OptionB;
}
// Use distributive conditional to create a union of objects
type Objective = TMethodKeys<Base> extends infer Key ? Key extends TMethodKeys<Base> ? {
baseKey: Key;
subKey: TMethodKeys<ReturnType<Base[Key]>>;
} : never : never;
const Test = [
{
baseKey: 'baseFunctionA',
subKey: 'aFunctionForOptionA'
},
{
baseKey: 'baseFunctionB',
subKey: 'thisIsAnotherFunctionForOptionB',
},
] as const satisfies Objective[];