Незначительное изменение: это происходило для меня в 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