Получить завершение типа на основе динамического (сопоставленного / условного) типа - PullRequest
0 голосов
/ 05 сентября 2018

Вы можете поместить следующий код в файл foo.ts. Я пытаюсь динамически генерировать типы. То, что я делаю, основано на этом вопросе: Сопоставить массив с интерфейсом

type TypeMapping = {
  Boolean: boolean,
  String: string,
  Number: number,
  ArrayOfString: Array<string>,
}

export enum Type {
  Boolean = 'Boolean',
  String = 'String',
  Number = 'Number',
  ArrayOfString = 'ArrayOfString'
}

const asOptions = <K extends Array<string>, T extends Array<{ name: K, type: keyof TypeMapping }>>(t: T) => t;

type OptionsToType<T extends Array<{ name: Array<string>, type: keyof TypeMapping }>>
  = { [K in T[number]['name'][0]]: TypeMapping[Extract<T[number], { name: K }>['type']] }


const options = asOptions([
  {
    name: ['foo'],
    type: Type.Boolean
  },

  {
    name: ['bar'],
    type: Type.String
  },

  {
    name: ['baz'],
    type: Type.Number
  },

  {
    name: ['bab'],
    type: Type.ArrayOfString
  }
]);



export type Opts = OptionsToType<typeof options>;

const v = <Opts>{foo: true};  // this does not compile

console.log(typeof v.foo);

Я не получаю никакого завершения типа - когда я печатаю v., ничего не появляется.

Ответы [ 2 ]

0 голосов
/ 05 сентября 2018

Предполагая, что вы используете первый элемент свойства name в качестве фактического ключа для добавления к результирующему типу, ваши определения немного неправильны. Я бы исправил их так:

const asOptions = <
  K extends string, 
  T extends Array<{ name: {0: K}, type: keyof TypeMapping }>
>(t: T) => t;

type OptionsToType<T extends Array<{ name: Array<string>, type: keyof TypeMapping }>> = {
  [K in T[number]['name'][0]]: TypeMapping[Extract<T[number], { name: {0: K} }>['type']] 
}

Различия:

  • Я все еще использую K extends string, чтобы вызвать вывод строкового литерала в asOptions. Есть мест , где TypeScript выводит строковые литералы и другие места, где он выводит только string, а K extends Array<string> не выводит строковый литерал. Так что K по-прежнему является строковым типом, и вместо этого я установил свойство name как {0: K}, которое обеспечит проверку первого элемента массива.

  • Снова в OptionsToType, K является строковым литералом, поэтому вы должны извлечь фрагмент T['number'], который имеет K в качестве первого элемента свойства name, то есть Extract<T[number], {name: {0: K} }>.

Остальное должно работать сейчас, я думаю. Надеюсь, это поможет. Удачи.

0 голосов
/ 05 сентября 2018

Вот пример, который использует Typescript 3 и объект в качестве входных данных. Я делаю нечто очень похожее на это в моих собственных проектах, чтобы сгенерировать упаковщик типизированных запросов для knex.js из моей базы данных Postgres.

// for lazier enum/mapping declaration
function StrEnum<T extends string[]>(...values: T) {
  let o = {};
  for (let v in values) {
    o[v] = v;
  }
  return o as { [K in T[number]]: K };
}
// declare enum values
const Type = StrEnum("Boolean", "String", "Number", "ArrayOfString");

// correlate the keys to things
type TypeMapping = {
  Boolean: boolean;
  String: string;
  Number: number;
  ArrayOfString: Array<string>;
};

// thing to convert your generated interface into something useful
const asOptions = <T extends { [key: string]: keyof TypeMapping }>(t: T) => t;

// the generated object
const options = asOptions({
  foo: Type.Boolean,
  bar: Type.String,
  baz: Type.Number,
  bab: Type.ArrayOfString
});

type Opts = Partial<
  { [V in keyof typeof options]: TypeMapping[typeof options[V]] }
>;

const v: Opts = { foo: true }; // this does compile

console.log(v);

Вот способ использования вашего текущего интерфейса:

// for lazier enum/mapping declaration
function StrEnum<T extends string[]>(...values: T) {
  let o = {};
  for (let v in values) {
    o[v] = v;
  }
  return o as { [K in T[number]]: K };
}
// declare enum values
const Type = StrEnum("Boolean", "String", "Number", "ArrayOfString");

// correlate the keys to things
type TypeMapping = {
  Boolean: boolean;
  String: string;
  Number: number;
  ArrayOfString: Array<string>;
};

type OptDefinitionElement<K extends string, V extends keyof TypeMapping> = {
  name: K;
  value: V;
};

// thing to convert your generated interface into something useful
const asOptions = <T extends OptDefinitionElement<any, any>[]>(...t: T) => {
  return t;
};

// because typescript doesn't like to infer strings
// nested inside objects/arrays so precisely
function InferString<S extends string>(s: S) {
  return s;
}

// the generated object
const options = asOptions(
  { name: InferString("foo"), value: Type.Boolean },
  { name: InferString("bar"), value: Type.String },
  { name: InferString("baz"), value: Type.Number },
  { name: "bab" as "bab", value: Type.ArrayOfString } // note you don't *have* to use the Infer helper
);

// way to iterate keys and construct objects, and then result in the | type of all
// of the values
type Values<T extends { [ignoreme: string]: any }> = T extends {
  [ignoreme: string]: infer R;
}
  ? R
  : never;
type OptionsType = typeof options;
type OptionKeys = Exclude<keyof OptionsType, keyof Array<any>>;
type Opts = Values<
  {
    [TupleIndex in Exclude<keyof OptionsType, keyof Array<any>>]: {
      [key in OptionsType[TupleIndex]["name"]]: TypeMapping[OptionsType[TupleIndex]["value"]]
    }
  }
>;

const v: Opts = { foo: true }; // this does compile

console.log(v);
...