Reputation: 182
I'm trying to find out if there is a way to infer types in an interface from an implementation's properties.
Simplified example:
interface Options {
type: 'string' | 'number'
demanded?: boolean
}
interface Command {
// The parameter options will contain the interpreted version of the options property
callback: (options: InferOptionTypings<this>) => void
options: { [key: string]: Options }
}
// Infer the options
// { type: 'string, demanded: false} | { type: 'string' } => string | undefined
// { type: 'string, demanded: true } => string
// { type: 'number', demanded: false} | { type: 'number } => number | undefined
// { type: 'number, demanded: true } => number
type InferOptionTypings<_ extends Command> = ... // here i've been stuck for very long
I've read the typings of yargs (and this is obviously inspired by yargs), but I've not figured out how to make it work in this style or what I'm missing/if this even is possible.
Example use case:
let command: Command = {
callback: (options) => {
options.a // string
options.b // number | undefined
options.c // string | undefined
options.d // error
},
options: {
a: {
type: 'string',
demanded: true,
},
b: {
type: 'number',
},
a: {
type: 'string',
},
},
}
Upvotes: 2
Views: 336
Reputation: 33041
It is possible, but in order to infer it you should create a function.
interface Option {
type: 'string' | 'number'
demanded?: boolean
}
/**
* Translates string type name to actual type
* Logic is pretty straitforward
*/
type TranslateType<T extends Option> =
T['type'] extends 'string'
? string
: T['type'] extends 'number'
? number
: never;
/**
* Check if demanded exists
* if true - apply never, because union of T|never produces T
* if false - apply undefined
*/
type ModifierType<T extends Option> =
T extends { demanded: boolean }
? T['demanded'] extends true
? never
: T['demanded'] extends false
? undefined
: never
: undefined
/**
* Apply TranslateType 'string' -> string
* Apply ModifierType {demanded:fale} -> undefined or never
*/
type TypeMapping<T extends Option> = TranslateType<T> | ModifierType<T>
/**
* Apply all conditions to each option
*/
type Mapping<T> = T extends Record<string, Option> ? {
[Prop in keyof T]: TypeMapping<T[Prop]>
} : never
type Data<Options> = {
callback: (options: Mapping<Options>) => void,
options: Options
}
const command = <
/**
* Infer each option
*/
Options extends Record<string, Option>
>(data: Data<Options>) => data
const result = command({
callback: (options) => {
type a = typeof options.a
type b = typeof options.b
type c = typeof options.c
options.a // string
options.b // number | undefined
options.c // string | undefined
options.d // error
},
options: {
a: {
type: 'string',
demanded: true,
},
b: {
type: 'number',
demanded: false
},
c: {
type: 'string',
},
},
})
I left the comments under each type utility
UPDATE Without function:
type WithoutFunction = Data<{
a: {
type: 'string',
demanded: true,
},
b: {
type: 'number',
demanded: false
},
c: {
type: 'string',
},
}>
Upvotes: 2