Tony Borchert
Tony Borchert

Reputation: 182

Infer types using own properties

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

Answers (1)

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

Playground

UPDATE Without function:

type WithoutFunction = Data<{
  a: {
    type: 'string',
    demanded: true,
  },
  b: {
    type: 'number',
    demanded: false
  },
  c: {
    type: 'string',
  },
}>

Upvotes: 2

Related Questions