typescript - Härleda typer med hjälp av egna egenskaper


Jag försöker ta reda på om det finns ett sätt att härleda typer i ett gränssnitt från en implementerings egenskaper.

Förenklat exempel:

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

Jag har läst typdefinitionerna för yargs (och detta är uppenbarligen inspirerat av yargs), men jag har inte lyckats lista ut hur man får det att fungera på detta sätt eller vad jag saknar/om detta ens är möjligt.

Exempel på användningsfall:

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

Svar #1

Det är möjligt, men för att kunna härleda det bör du skapa en funktion.

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

Jag lämnade kommentarerna under varje typ av verktyg

Lekplats

UPPDATERING Utan funktion:

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