WedX - журнал о программировании и компьютерных науках

Как создать сопоставленные типы, производные от других сопоставленных типов с назначаемыми ключами

Незначительное изменение: это происходило для меня в TS 3.0.1.

У меня возникли проблемы с использованием Typescript для определения формы конфигурации для использования с усилителем компонентов React. По сути, я хочу указать объект карты, свойства которого используются усилителем для создания внедренных свойств для расширенного компонента.

Там, где я сталкивался с проблемами, похоже, я пытался создать сопоставленные типы, производные от других сопоставленных типов. Всего по базовому энхансеру у меня есть такие отводы:

  • Given a base config, whose props could be one of two shapes...
    • Create a normalized config whose props are the same as the props of the base config, but only of one shape.
    • Реквизиты, которые являются функциями, по одной для каждой опоры базовой конфигурации.
    • Реквизиты, которые являются значениями, по одному для каждой опоры базовой конфигурации.
    • Функция mapDispatchToProps, в качестве возвращаемого типа которой использовались свойства, полученные из немного другой версии config.

И для предварительно составленного усилителя, созданного для общего случая использования:

  • Получение базовой конфигурации из этой конфигурации с учетом диспетчеризации.
  • Невозможность присвоения ключей: Type 'Extract<keyof TBase, string>' is not assignable to type 'Extract<keyof TRel, string>'.

Вот проблемы, с которыми я обычно сталкиваюсь:

  • Запрос сервера языка TypeScript или компилятор умирает с RangeError: Maximum call stack size exceeded, вероятно, из-за большого количества условных типов и сопоставленных типов.
  • Non-indexability of base type when derived from related type: Type 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'.
    • Basically the same as the first one but at the point of assignment.
  • Возможно ли это сделать в TypeScript?

Мои вопросы таковы:

  • Если так, то есть лучший способ сделать это, чем то, что я сделал, придерживаясь простых объектов и сохраняя ограничения типа для каждого пропа?
  • Почему два набора ключей-союзов, производных от двух сопоставленных типов, не назначаются, когда они оба в конечном итоге являются производными только одного сопоставленного типа? Причина в том, что, хотя они оба являются производными от одного, TS оценивает возможность присваивания ключей-объединений только ограничениями параметров, а не конкретными экземплярами типов? Или что-то еще мне не хватает?
  • Я не понимаю конечной цели; пример ввода и вывода был бы действительно полезен. Что касается конкретной ошибки mapDispatchToProps, это связано с тем, что Type 'Extract<keyof TBase, string>' is not assignable to type 'Extract<keyof TRel, string>'. и Type 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'. являются независимыми параметрами типа; RangeError: Maximum call stack size exceeded имеет значение по умолчанию, но его может изменить вызывающий абонент. Так что ничто не мешает
    // NOTE: Crashes the playground in chrome on my computer:
    //   RangeError: Maximum call stack size exceeded
    // Also probably crashes a tsserver process/request/thing because
    // vscode stops updating the error squigglies after a bit.
    
    // Convenience.
    type PropKeyOf<T> = Extract<keyof T, string>;
    
    // A "related" type that I want to be related to a "base" type.
    // Particularly, I want to be able to derive the "base" type.
    // "related" and "base" are used here because these are config types
    // for the interface of a related enhancer utility and
    // a base enhancer utility respectively.
    // They are otherwise unrelated.
    
    type RelatedMap<T> = {
      [K in PropKeyOf<T>]: RelatedMapPropType<T[K]>;
    };
    
    type RelatedMapPropType<T> = T extends RelatedMapProp<infer V> ? RelatedMapProp<V> : never;
    
    type RelatedMapProp<V> = { foo: V, init(): V };
    
    // A "base" type that I want to use for a "base" interface.
    
    type BaseMap<T> = {
      [K in PropKeyOf<T>]: BaseMapPropType<T[K]>;
    };
    
    type BaseMapPropType<T> = T extends BaseMapProp<infer V> ? BaseMapProp<V> : never;
    
    type BaseMapProp<V> = { baz: V, init(): V };
    
    // Make the conversion type
    type BaseMapOfRelatedMap<TRel extends RelatedMap<TRel>> = {
      [K in PropKeyOf<TRel>]: BasePropOfRelatedMapProp<TRel[K]>;
    }
    
    type BasePropOfRelatedMapProp<TRelProp> = TRelProp extends RelatedMapProp<infer V> ? BaseMapProp<V> : never;
    
    function isOwnProp<O extends {}>(o: O, pn: string): pn is PropKeyOf<O> {
      return !!o && (typeof o === 'object') && Object.prototype.hasOwnProperty.call(o, pn);
    }
    
    function createBaseMapOfRelatedMap<
      TRel extends RelatedMap<TRel>,
      // Error:
      // - [ts] Type 'BaseMapOfRelatedMap<TRel>' does not satisfy the constraint 'BaseMap<TBase>'.
      //   - Type 'Extract<keyof TBase, string>' is not assignable to
      //     type 'Extract<keyof TRel, string>'.
      TBase extends BaseMap<TBase> = BaseMapOfRelatedMap<TRel>
    >(foo: TRel): TBase {
      const baz = {} as TBase;
    
      for (const propName in foo) if (isOwnProp(foo, propName)) {
        // Errors:
        // - [ts] Type 'Extract<keyof TRel, string>' cannot be used
        //   to index type 'TBase'.
        // - [ts] Property 'foo' does not exist
        //   on type 'TRel[Extract<keyof TRel, string>]'.
        baz[propName] = { baz: foo[propName].foo, init: foo[propName].init };
      }
    
      return baz;
    }
    
    иметь свойства, которых нет у TBase. Например, вызывающий абонент может:

Вот небольшой пример, который, кажется, выявляет основные ошибки, с которыми я сталкиваюсь:

Спасибо за помощь, Мэтт!

// NOTE: Crashes the playground in chrome on my computer:
//   RangeError: Maximum call stack size exceeded
// Also probably crashes a tsserver process/request/thing because
// vscode stops updating the error squigglies after a bit.

// Convenience.
type PropKeyOf<T> = Extract<keyof T, string>;

// A "related" type that I want to be related to a "base" type.
// Particularly, I want to be able to derive the "base" type.
// "related" and "base" are used here because these are config types
// for the interface of a related enhancer utility and
// a base enhancer utility respectively.
// They are otherwise unrelated.

type RelatedMap<T> = {
  [K in PropKeyOf<T>]: RelatedMapPropType<T[K]>;
};

type RelatedMapPropType<T> = T extends RelatedMapProp<infer V> ? RelatedMapProp<V> : never;

type RelatedMapProp<V> = { foo: V, init(): V };

// A "base" type that I want to use for a "base" interface.

type BaseMap<T> = {
  [K in PropKeyOf<T>]: BaseMapPropType<T[K]>;
};

type BaseMapPropType<T> = T extends BaseMapProp<infer V> ? BaseMapProp<V> : never;

type BaseMapProp<V> = { baz: V, init(): V };

// Make the conversion type
type BaseMapOfRelatedMap<TRel extends RelatedMap<TRel>> = {
  [K in PropKeyOf<TRel>]: BasePropOfRelatedMapProp<TRel[K]>;
}

type BasePropOfRelatedMapProp<TRelProp> = TRelProp extends RelatedMapProp<infer V> ? BaseMapProp<V> : never;

function isOwnProp<O extends {}>(o: O, pn: string): pn is PropKeyOf<O> {
  return !!o && (typeof o === 'object') && Object.prototype.hasOwnProperty.call(o, pn);
}

function createBaseMapOfRelatedMap<
  TRel extends RelatedMap<TRel>,
  // Error:
  // - [ts] Type 'BaseMapOfRelatedMap<TRel>' does not satisfy the constraint 'BaseMap<TBase>'.
  //   - Type 'Extract<keyof TBase, string>' is not assignable to
  //     type 'Extract<keyof TRel, string>'.
  TBase extends BaseMap<TBase> = BaseMapOfRelatedMap<TRel>
>(foo: TRel): TBase {
  const baz = {} as TBase;

  for (const propName in foo) if (isOwnProp(foo, propName)) {
    // Errors:
    // - [ts] Type 'Extract<keyof TRel, string>' cannot be used
    //   to index type 'TBase'.
    // - [ts] Property 'foo' does not exist
    //   on type 'TRel[Extract<keyof TRel, string>]'.
    baz[propName] = { baz: foo[propName].foo, init: foo[propName].init };
  }

  return baz;
}

Исходный контекст

ПРИМЕЧАНИЕ: исправлены примеры имен.

Что касается конкретной ошибки 'Extract<keyof TRel, string>' cannot be used to index type 'TBase', это потому, что TRel и TBase являются независимыми параметрами типа; TBase имеет значение по умолчанию, но может быть изменено вызывающим абонентом. Так что ничто не мешает TRel иметь свойства, которых нет у TBase.

On TBase

В этом есть смысл, хороший момент, я действительно не думал об этом в то время, вроде как глубоко погрузился в голову в одном образе мышления. Думаю, это означает, что я не могу использовать параметры типа, чтобы сократить это, если я не хочу добавить больше extends ... ограничений.

Ну вот так:

К сожалению, это снова приводит к сбою запроса tsserver:

// added to try to typecheck created prop.
function createBasePropOfRelatedMapProp<
  TRelProp extends RelatedMapProp<TRelProp>,
>(fooProp: TRelProp): BasePropOfRelatedMapProp<TRelProp> {
  return { baz: fooProp.foo, init: fooProp.init };
}

function createBaseMapOfRelatedMap<
  TRel extends RelatedMap<TRel>,
>(foo: TRel): BaseMapOfRelatedMap<TRel> {
  const baz = {} as BaseMapOfRelatedMap<TRel>;

  for (const propName in foo) if (isOwnProp(foo, propName)) {
    baz[propName] = createBasePropOfRelatedMapProp(foo[propName]);
  }

  return baz;
}

function logBaseMap<TBase extends BaseMap<TBase>>(base: TBase): void {
  for (const propName in base) if (isOwnProp(base, propName)) {
    console.log(propName, '=>', base[propName]);
  }
}

Увы.

Err 551   [15:35:42.708] Exception on executing command delayed processing of request 12:

    Maximum call stack size exceeded

    RangeError: Maximum call stack size exceeded
    at getSimplifiedIndexedAccessType (/.../client/node_modules/typescript/lib/tsserver.js:37544:48)
    at getSimplifiedType (/.../client/node_modules/typescript/lib/tsserver.js:37540:63)
    at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:54)
    at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
    at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
    at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:34)
    at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
    at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
    (... repeat ad nauseum)

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

Создайте компонент типа props, который имеет ...

Исходный код, по сути, работает примерно так:

Затем я хотел использовать ограничения сопоставленного типа, чтобы гарантировать, что все реквизиты на config имеют один и тот же тип OwnProps, и что каждая сама будет внутренне согласована в отношении используемых в ней типов, что в основном заметно в bar, где, например, reduce должен возвращать тот же тип, что и его аргумент prevPropValue, и этот initial также должен возвращать тот же тип; но также, что последний аргумент массива для reduce является кортежем типов args функции, возвращаемой request.

const config = {
  // sometimes I only need just the request itself.
  foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),

  // sometimes I need more control.
  bar: {
    request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
    reduce: (
      prevPropValue: { [k: string]: AsyncData<APIResponse> },
      nextResValue: AsyncData<APIResponse>,
      ownProps: ComponentOwnProps,
      [barId]: [string]
    ) => ({
      ...prevPropValue,
      [barId]: nextResValue,
    }),
    initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
  },
};

const enhanceComponent = withAsyncData(config);

Как часть этого, мне нужно было затем сгенерировать тип для свойств, которые вводятся этой конфигурацией:

Затем я захотел вариант вышеупомянутой конфигурации для использования с предварительной компоновкой withAsyncData с connect React-Redux, которая в итоге выглядела так:

  • props.getAsyncData.foo(): Promise<AsyncData<APIResponse>>
  • props.getAsyncData.bar(barId: string): Promise<AsyncData<APIResponse>>
  • props.asyncData.foo: AsyncData<APIResponse>
  • props.asyncData.bar: AsyncData<APIResponse>

Предварительная композиция (по сути) просто config => compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config))). Но, конечно, мне нужно создать базовый тип конфигурации, производный от этого (слегка) расширенного типа конфигурации, используя createAsyncDataConfig().

const config = {
  foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
  bar: {
    request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
    reduce: (
      prevPropValue: { [k: string]: AsyncData<APIResponse> },
      nextResValue: AsyncData<APIResponse>,
      ownProps: ComponentOwnProps,
      [barId]: [string]
    ) => ({
      ...prevPropValue,
      [barId]: nextResValue,
    }),
    initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
  },
};

const enhanceComponent = withConnectedAsyncData(config);

Редактировать 1


  • Изменение BaseMapOfRelatedMap только на _2_ и _3_ на _4_, похоже, больше не увеличивает производительность, хотя второе действительно устраняет первую _5_ ошибку. Однако не исправляет ошибку _6_. 14.08.2018
  • Я могу воспроизвести сбой в текущей type BaseMapOfRelatedMap<TRel extends RelatedMap<TRel>> = ... версии TypeScript. Я не вижу существующей ошибки сопоставления, хотя вполне возможно, что github.com/Microsoft/TypeScript/ issues / 22950 могут быть связаны. Хотите подать вопрос или мне? На остальные вопросы отвечу отдельно. 14.08.2018
  • Я подал github.com/Microsoft/TypeScript/issues/26448. Ха, вопрос о переполнении стека, который на самом деле относится к переполнению стека! 14.08.2018
  • Благодарим за подачу заявки! Я видел эту другую проблему, но не был уверен, действительно ли она связана с моим случаем из-за разных имен в трассировке стека и разных версий TS. (Я использую 3.0.1, кое-что я, кажется, забыл упомянуть в своем вопросе ...) 14.08.2018
  • Спасибо что подметил это. Обновил свой вопрос ответом. Это отвечает на вопрос об индексируемости / назначении клавиш. Но пока не могу сказать, все ли это отвечает. 14.08.2018

Ответы:


1

И код попытается проиндексировать baz со свойством x, которого у него нет.

createBazOfRelatedMap<{x: number}, {}>(...);

Это работает для меня как решение исходной проблемы и пока не привело к сбою компилятора:

На первый взгляд кажется, что вы пытаетесь неправильно использовать 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'. Я подозреваю, что ваш стек вызовов взрывается, потому что вы пытаетесь объявить класс, расширяющий универсальный, который использует себя.

Раунд 2

// DUMMY DECLARATIONS
interface AsyncData<T> {
  asyncDataMarker: T;
}
interface APIResponse {
  apiResponseMarker: undefined;
}
declare function apiFetch(url: string): AsyncData<APIResponse>;
interface ComponentOwnProps {
  fooId: string;
}
interface AppDispatch {
  appDispatchMarker: undefined;
}

// FIRST VERSION

type SimpleConfigEntry<OwnProps, Response> = (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry<OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
  request: (ownProps: OwnProps) => (...args: RequestArgs) => Response,
  reduce: (
    prevPropValue: PropValue,
    nextResValue: Response,
    ownProps: OwnProps,
    args: RequestArgs
  ) => PropValue,
  initial: () => PropValue
};

type CheckConfigEntry<OwnProps, T> = 
  T extends ComplexConfigEntry<OwnProps, infer RequestArgs, infer Response, infer PropValue>
    ? (ComplexConfigEntry<OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
    : T extends SimpleConfigEntry<OwnProps, infer Response>
      ? (SimpleConfigEntry<OwnProps, Response> extends T ? T : never)
      : never;

type ConfigEntryCommonInferrer<OwnProps, Response> =
  ((ownProps: OwnProps) => () => Response) | {request: (ownProps: OwnProps) => (...args: any[]) => Response};

declare function withAsyncData
  <OwnProps, C extends {[K in keyof C]: CheckConfigEntry<OwnProps, C[K]>}>
  (config: C & {[k: string]: ConfigEntryCommonInferrer<OwnProps, any>}): /*TODO*/ unknown;

type InjectedProps<C> = {
  getAsyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Promise<Response> : unknown},
  asyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Response : unknown}
}

// Example

const config = {
  // sometimes I only need just the request itself.
  foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),

  // sometimes I need more control.
  bar: {
    request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
    reduce: (
      prevPropValue: { [k: string]: AsyncData<APIResponse> },
      nextResValue: AsyncData<APIResponse>,
      ownProps: ComponentOwnProps,
      [barId]: [string]
    ) => ({
      ...prevPropValue,
      [barId]: nextResValue,
    }),
    initial: () => ({} as { [k: string]: AsyncData<APIResponse> }),
  },
};

const enhanceComponent = withAsyncData(config);
type ExampleInjectedProps = InjectedProps<typeof config>;

// SECOND VERSION

type SimpleConfigEntry2<Dispatch, OwnProps, Response> = (dispatch: Dispatch) => (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
  request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: RequestArgs) => Response,
  reduce: (
    prevPropValue: PropValue,
    nextResValue: Response,
    ownProps: OwnProps,
    args: RequestArgs
  ) => PropValue,
  initial: () => PropValue
};

type CheckConfigEntry2<Dispatch, OwnProps, T> = 
  T extends ComplexConfigEntry2<Dispatch, OwnProps, infer RequestArgs, infer Response, infer PropValue>
    ? (ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
    : T extends SimpleConfigEntry2<Dispatch, OwnProps, infer Response>
      ? (SimpleConfigEntry2<Dispatch, OwnProps, Response> extends T ? T : never)
      : never;

type ConfigEntryCommonInferrer2<Dispatch, OwnProps, Response> =
  ((dispatch: Dispatch) => (ownProps: OwnProps) => () => Response) |
  {request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: any[]) => Response};

declare function withConnectedAsyncData
  <Dispatch, OwnProps, C extends {[K in keyof C]: CheckConfigEntry2<Dispatch, OwnProps, C[K]>}>
  (config: C & {[k: string]: ConfigEntryCommonInferrer2<Dispatch, OwnProps, any>}): /*TODO*/ unknown;

// Example

const config2 = {
  foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
  bar: {
    request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
    reduce: (
      prevPropValue: { [k: string]: AsyncData<APIResponse> },
      nextResValue: AsyncData<APIResponse>,
      ownProps: ComponentOwnProps,
      [barId]: [string]
    ) => ({
      ...prevPropValue,
      [barId]: nextResValue,
    }),
    initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
  },
};

const enhanceComponent2 = withConnectedAsyncData(config2);
14.08.2018
  • У меня еще не было времени протестировать это, приближались сроки и все такое, но я решил принять это, поскольку, глядя на него, если это не полное решение, по крайней мере, лучшее место для начала. Спасибо! 14.08.2018
  • РЕДАКТИРОВАТЬ: Ссылка на игровую площадку TS с этим в нем ( это просто приводит к сбою компилятора на моем компьютере / браузере) 20.08.2018
  • Новые материалы

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

    Учебные заметки: создание моего первого пакета Node.js
    Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

    Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
    Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..

    ИИ в аэрокосмической отрасли
    Каждый полет – это шаг вперед к великой мечте. Чтобы это происходило в их собственном темпе, необходима команда астронавтов для погони за космосом и команда технического обслуживания..


    Для любых предложений по сайту: [email protected]