Получить завершение типа на основе динамического (сопоставленного/условного) типа

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

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., ничего не появляется.


person Alexander Mills    schedule 04.09.2018    source источник
comment
вы твердо настроены на этот интерфейс? Когда я создаю такие вещи, объекты (по сравнению с массивами), как правило, намного проще, потому что работает итерация ключа до TS3. Я составлю такой ответ.   -  person Catalyst    schedule 05.09.2018
comment
Почему вы делаете свойство name массивом? В вашем примере используется только одноэлементный массив (один кортеж)... если это всегда только один элемент, почему бы вместо этого не использовать просто строку? Если иногда это более одного элемента, можете ли вы показать, как вы хотите его использовать?   -  person jcalz    schedule 05.09.2018
comment
Это для удобства пользователя - они определяют массив параметров, первый элемент - это имя, остальные - псевдонимы.   -  person Alexander Mills    schedule 05.09.2018
comment
@AlexanderMills обновлен, чтобы не слишком сильно возиться с вашей формой ввода, трюк с итерацией кортежа состоит в том, чтобы исключить ключи в типе Array‹any›. Обратите внимание, что вам нужно будет заставить машинописный текст правильно распознавать строки вашего имени. Он имеет тенденцию бороться со мной, когда я делаю такие вещи.   -  person Catalyst    schedule 05.09.2018


Ответы (2)


Вот пример, который использует 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);
person Catalyst    schedule 05.09.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} }>.

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

person jcalz    schedule 05.09.2018