Сужение возвращаемого типа из общего дискриминируемого объединения в TypeScript
у меня есть метод класса, который принимает один аргумент в качестве строки и возвращает объект, который имеет соответствие type
собственность. Этот метод используется для сужения различаемого типа объединения вниз и гарантирует, что возвращаемый объект всегда будет определенного суженного типа, который имеет предоставленный type
значение различий.
я пытаюсь предоставить сигнатуру типа для этого метода, которая правильно сузит тип от общего параметра, но ничего I try сужает его от дискриминируемого объединения без явного предоставления пользователем типа, к которому он должен быть сужен. Это работает, но раздражает и чувствует себя совершенно излишним.
надеюсь, это минимальное воспроизведение дает понять:
interface Action {
type: string;
}
interface ExampleAction extends Action {
type: 'Example';
example: true;
}
interface AnotherAction extends Action {
type: 'Another';
another: true;
}
type MyActions = ExampleAction | AnotherAction;
declare class Example<T extends Action> {
// THIS IS THE METHOD IN QUESTION
doSomething<R extends T>(key: R['type']): R;
}
const items = new Example<MyActions>();
// result is guaranteed to be an ExampleAction
// but it is not inferred as such
const result1 = items.doSomething('Example');
// ts: Property 'example' does not exist on type 'AnotherAction'
console.log(result1.example);
/**
* If the dev provides the type more explicitly it narrows it
* but I'm hoping it can be inferred instead
*/
// this works, but is not ideal
const result2 = items.doSomething<ExampleAction>('Example');
// this also works, but is not ideal
const result3: ExampleAction = items.doSomething('Example');
я также попытался стать умным, пытаясь создать" сопоставленный тип " динамически-что является довольно новой функцией в TS.
declare class Example2<T extends Action> {
doSomething<R extends T['type'], TypeMap extends { [K in T['type']]: T }>(key: R): TypeMap[R];
}
это страдает от того же результата: он не сужает тип потому что в карте типа { [K in T['type']]: T }
значение для каждого вычисляемого свойства, T
, не для каждого свойства K in
шаг а так же MyActions
Союза. Если я требую, чтобы пользователь предоставил предопределенный сопоставленный тип, который я могу использовать, это сработает, но это не вариант, поскольку на практике это будет очень плохой опыт разработчика. (профсоюзы огромны)
этот вариант использования может показаться странным. Я попытался дистиллировать свою проблему в более потребляемую форма, но мой вариант использования на самом деле касается наблюдаемых. Если вы знакомы с ними, я пытаюсь более точно ввести ofType
оператор, предоставленный redux-observable. Это в основном стенография для filter()
на type
свойства.
это на самом деле очень похоже на то, как Observable#filter
и Array#filter
также сужают типы, но TS, похоже, выясняет это, потому что обратные вызовы предикатов имеют value is S
возвращаемое значение. Непонятно, как Я мог бы адаптировать что-то подобное здесь.
5 ответов
как и многие хорошие решения в программировании, вы достигаете этого, добавляя слой косвенности.
в частности, мы можем добавить таблицу между тегами действий (т. е. "Example"
и "Another"
) и их грузоподъемность.
type ActionPayloadTable = {
"Example": { example: true },
"Another": { another: true },
}
тогда мы можем создать вспомогательный тип, который помечает каждую полезную нагрузку определенным свойством, которое сопоставляется с каждым тегом действия:
type TagWithKey<TagName extends string, T> = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
, который мы будем использовать для создания таблицы между действием и полное действие самих объектов:
type ActionTable = TagWithKey<"type", ActionPayloadTable>;
это был более простой (хотя и менее ясный) способ написания:
type ActionTable = {
"Example": { type: "Example" } & { example: true },
"Another": { type: "Another" } & { another: true },
}
теперь мы можем создавать удобные имена для каждого из действий:
type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
и мы можем либо создать союз, написав
type MyActions = ExampleAction | AnotherAction;
или мы можем избавить себя от обновления Союза каждый раз, когда мы добавляем новое действие, написав
type Unionize<T> = T[keyof T];
type MyActions = Unionize<ActionTable>;
наконец, мы можем перейти к класс у вас. Вместо параметризуя действия, мы вместо этого параметризуем таблицу действий.
declare class Example<Table> {
doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}
это, вероятно, та часть, которая будет иметь наибольший смысл - Example
в основном просто сопоставляет входы вашей таблицы с ее выходами.
в целом, вот код.
/**
* Adds a property of a certain name and maps it to each property's key.
* For example,
*
* ```
* type ActionPayloadTable = {
* "Hello": { foo: true },
* "World": { bar: true },
* }
*
* type Foo = TagWithKey<"greeting", ActionPayloadTable>;
* ```
*
* is more or less equivalent to
*
* ```
* type Foo = {
* "Hello": { greeting: "Hello", foo: true },
* "World": { greeting: "World", bar: true },
* }
* ```
*/
type TagWithKey<TagName extends string, T> = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
type Unionize<T> = T[keyof T];
type ActionPayloadTable = {
"Example": { example: true },
"Another": { another: true },
}
type ActionTable = TagWithKey<"type", ActionPayloadTable>;
type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
type MyActions = Unionize<ActionTable>
declare class Example<Table> {
doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}
const items = new Example<ActionTable>();
const result1 = items.doSomething("Example");
console.log(result1.example);
для этого требуется изменения в TypeScript работать точно так, как задано в вопросе.
если классы могут быть сгруппированы как свойства одного объекта, то принятый ответ также может помочь. Я люблю Unionize<T>
трюк там.
чтобы объяснить реальную проблему, позвольте мне сузить ваш пример до этого:
class RedShape {
color: 'Red'
}
class BlueShape {
color: 'Blue'
}
type Shapes = RedShape | BlueShape;
type AmIRed = Shapes & { color: 'Red' };
/* Equals to
type AmIRed = (RedShape & {
color: "Red";
}) | (BlueShape & {
color: "Red";
})
*/
/* Notice the last part in before:
(BlueShape & {
color: "Red";
})
*/
// Let's investigate:
type Whaaat = (BlueShape & {
color: "Red";
});
type WhaaatColor = Whaaat['color'];
/* Same as:
type WhaaatColor = "Blue" & "Red"
*/
// And this is the problem.
еще одна вещь, которую вы можете сделать, это передать фактический класс функции. Вот такой сумасшедший пример:
declare function filterShape<
TShapes,
TShape extends Partial<TShapes>
>(shapes: TShapes[], cl: new (...any) => TShape): TShape;
// Doesn't run because the function is not implemented, but helps confirm the type
const amIRed = filterShape(new Array<Shapes>(), RedShape);
type isItRed = typeof amIRed;
/* Same as:
type isItRed = RedShape
*/
проблема здесь в том, что вы не можете получить значение color
. Ты можешь!--9-->, но это всегда будет неопределенным, потому что значение применяется только в конструкторе. RedShape
составляется:
var RedShape = /** @class */ (function () {
function RedShape() {
}
return RedShape;
}());
и даже если у вас:
class RedShape {
color: 'Red' = 'Red';
}
это компилируется в:
var RedShape = /** @class */ (function () {
function RedShape() {
this.color = 'Red';
}
return RedShape;
}());
и в вашем реальном примере конструкторы могут иметь несколько параметров и т. д., Поэтому создание экземпляра также может быть невозможно. Не говоря уже о его не работа для интерфейсов тоже.
возможно, Вам придется вернуться к глупому как:
class Action1 { type: '1' }
class Action2 { type: '2' }
type Actions = Action1 | Action2;
declare function ofType<TActions extends { type: string },
TAction extends TActions>(
actions: TActions[],
action: new(...any) => TAction, type: TAction['type']): TAction;
const one = ofType(new Array<Actions>(), Action1, '1');
/* Same as if
var one: Action1 = ...
*/
или в doSomething
текст:
declare function doSomething<TAction extends { type: string }>(
action: new(...any) => TAction, type: TAction['type']): TAction;
const one = doSomething(Action1, '1');
/* Same as if
const one : Action1 = ...
*/
как упоминалось в комментарии к другому ответу, в TypeScript есть проблема для исправления проблемы вывода. Я написал комментарий, ссылающийся на объяснение этого ответа и предоставляющий пример более высокого уровня проблемы здесь.
к сожалению, вы не можете достичь этого поведения, используя тип объединения (т. е. type MyActions = ExampleAction | AnotherAction;
).
тем не менее, ваше решение отлично. Вам просто нужно использовать этот способ, чтобы определить тип, который вам нужен.
const result2 = items.doSomething<ExampleAction>('Example');
хотя вам не нравится это, это кажется довольно законным способом сделать то, что вы хотите.
немного подробнее о настройке, но мы можем достичь желаемого API с помощью поиска типов:
interface Action {
type: string;
}
interface Actions {
[key: string]: Action;
}
interface ExampleAction extends Action {
type: 'Example';
example: true;
}
interface AnotherAction extends Action {
type: 'Another';
another: true;
}
type MyActions = {
Another: AnotherAction;
Example: ExampleAction;
};
declare class Example<T extends Actions> {
doSomething<K extends keyof T, U>(key: K): T[K];
}
const items = new Example<MyActions>();
const result1 = items.doSomething('Example');
console.log(result1.example);
начиная с TypeScript 2.8, вы можете выполнить это с помощью условных типов.
// Narrows a Union type base on N
// e.g. NarrowAction<MyActions, 'Example'> would produce ExampleAction
type NarrowAction<T, N> = T extends { type: N } ? T : never;
interface Action {
type: string;
}
interface ExampleAction extends Action {
type: 'Example';
example: true;
}
interface AnotherAction extends Action {
type: 'Another';
another: true;
}
type MyActions =
| ExampleAction
| AnotherAction;
declare class Example<T extends Action> {
doSomething<K extends T['type']>(key: K): NarrowAction<T, K>
}
const items = new Example<MyActions>();
// Inferred ExampleAction works
const result1 = items.doSomething('Example');
Примечание: кредит @jcalz для идеи типа NarrowAction из этого ответа https://stackoverflow.com/a/50125960/20489