使用自己的属性推断类型

Infer types using own properties

我正在尝试找出是否有一种方法可以从实现的属性推断接口中的类型。

简化示例:

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

我读过 yargs 的类型(这显然是受 yargs 的启发),但我还没有想出如何让它以这种风格工作或者我什至 missing/if是可以的。

示例用例:

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',
    },
  },
}

这是可能的,但为了推断它,您应该创建一个函数。

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',
    },
  },
})

我在每种实用程序下留下了评论

Playground

更新 无功能:

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