Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
У меня есть метод класса, который принимает 9X_discriminated-union один аргумент в виде строки и возвращает 9X_typescript объект, имеющий соответствующее свойство 9X_typescript-types type
. Этот метод используется для сужения различаемого 9X_typescript-types типа объединения и гарантирует, что возвращаемый 9X_atscript объект всегда будет иметь конкретный суженный 9X_typescript-types тип, который имеет предоставленное значение 9X_typescript-typings дискриминации type
.
Я пытаюсь предоставить сигнатуру 9X_typescript типа для этого метода, которая будет правильно 9X_atscript сужать тип до общего параметра, но ничто 9X_discriminated-union из того, что я пытаюсь не сузить, сужает 9X_discriminated-union его от размеченного объединения без явного 9X_typescript-types указания пользователем типа, который следует 9X_typescript-types сузить к. Это работает, но раздражает и 9X_vanilla-typescript кажется излишним.
Надеюсь, это минимальное 9X_discriminated-union воспроизведение проясняет:
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 {
// THIS IS THE METHOD IN QUESTION
doSomething(key: R['type']): R;
}
const items = new Example();
// 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('Example');
// this also works, but is not ideal
const result3: ExampleAction = items.doSomething('Example');
Я также попытался 9X_typescript-types сообразить, пытаясь создать "сопоставленный 9X_typescript-typings тип" динамически - это довольно новая 9X_typescript функция в TS.
declare class Example2 {
doSomething(key: R): TypeMap[R];
}
Это имеет тот же результат: он 9X_typescript не сужает тип, потому что в карте типов 9X_typescript-typings { [K in T['type']]: T }
значение для каждого вычисляемого свойства 9X_typescript-types T
не равно для каждого свойства итерации K in
., но вместо этого представляет 9X_discriminated-union собой тот же союз MyActions
. Если мне нужно, чтобы 9X_typescript-typings пользователь предоставил предопределенный 9X_typescript-types сопоставленный тип, который я могу использовать, это 9X_typescript-types сработает, но это не вариант, поскольку 9X_typescript на практике это было бы очень плохим опытом 9X_typescript-types разработчика. (профсоюзы огромны)
Этот вариант 9X_typescript-typings использования может показаться странным. Я 9X_discriminated-union попытался преобразовать свою проблему в 9X_discriminated-union более удобную форму, но мой вариант использования 9X_typescript-types на самом деле касается Observables. Если 9X_typescript вы знакомы с ними, я пытаюсь более точно 9X_typescript ввести ofType
operator provided by redux-observable. По сути, это сокращение от filter()
on the type
property.
На 9X_vanilla-typescript самом деле это очень похоже на то, как Observable#filter
и 9X_typescript-types Array#filter
также сужают типы, но TS, кажется, понимает 9X_vanilla-typescript это, потому что обратные вызовы предикатов 9X_typescript-types имеют возвращаемое значение value is S
. Непонятно, как 9X_atscript я мог адаптировать здесь что-то подобное.
Ответ #1
Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
Как и во многих хороших решениях в программировании, вы 9X_vanilla-typescript добиваетесь этого, добавляя уровень косвенности.
В 9X_atscript частности, здесь мы можем добавить таблицу 9X_discriminated-union между тегами действий (т.е. "Example"
и "Another"
) и их соответствующими 9X_discriminated-union полезными нагрузками.
type ActionPayloadTable = {
"Example": { example: true },
"Another": { another: true },
}
тогда мы можем создать 9X_typescript-typings вспомогательный тип, который помечает каждую 9X_discriminated-union полезную нагрузку определенным свойством, которое 9X_typescript сопоставляется с каждым тегом действия:
type TagWithKey = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
Что 9X_typescript-types мы будем использовать для создания таблицы 9X_typescript-typings между типами действий и самими полными объектами 9X_typescript действий:
type ActionTable = TagWithKey<"type", ActionPayloadTable>;
Это был более простой (хотя и менее 9X_atscript понятный) способ написания:
type ActionTable = {
"Example": { type: "Example" } & { example: true },
"Another": { type: "Another" } & { another: true },
}
Теперь мы можем 9X_atscript создавать удобные названия для каждого из 9X_discriminated-union наших действий:
type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
И мы можем создать союз, написав
type MyActions = ExampleAction | AnotherAction;
или 9X_typescript мы можем избавить себя от обновления объединения 9X_discriminated-union каждый раз, когда добавляем новое действие, написав
type Unionize = T[keyof T];
type MyActions = Unionize;
Наконец, мы 9X_discriminated-union можем перейти к вашему занятию. Вместо параметризации 9X_atscript действий мы будем параметризовать таблицу 9X_typescript-types действий.
declare class Example {
doSomething(key: ActionName): Table[ActionName];
}
Вероятно, именно эта часть будет 9X_typescript-types иметь наибольший смысл - Example
в основном просто 9X_atscript сопоставляет входы вашей таблицы с ее выходами.
В 9X_typescript-typings общем, вот код.
/**
* 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 = {
[K in keyof T]: { [_ in TagName]: K } & T[K]
};
type Unionize = 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
declare class Example {
doSomething(key: ActionName): Table[ActionName];
}
const items = new Example();
const result1 = items.doSomething("Example");
console.log(result1.example);
38
I
Isixones
- Есть ли причина, по к ...
Ответ #2
Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
Начиная с TypeScript 2.8, это можно сделать 9X_typescript с помощью условных типов.
// Narrows a Union type base on N
// e.g. NarrowAction would produce ExampleAction
type NarrowAction = 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 {
doSomething(key: K): NarrowAction
}
const items = new Example();
// Inferred ExampleAction works
const result1 = items.doSomething('Example');
ПРИМЕЧАНИЕ. Благодарим 9X_typescript @jcalz за идею типа NarrowAction из этого 9X_atscript ответа https://stackoverflow.com/a/50125960/20489
20
E
Eclipxs
Ответ #3
Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
Для этого требуется, чтобы a change in TypeScript работал точно 9X_vanilla-typescript так, как задано в вопросе.
Если классы можно 9X_typescript сгруппировать как свойства одного объекта, принятый 9X_discriminated-union ответ тоже может помочь. Мне нравится трюк 9X_vanilla-typescript Unionize
.
Чтобы объяснить реальную проблему, позвольте 9X_atscript мне сузить ваш пример до следующего:
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.
Еще 9X_typescript-types вы могли бы передать фактический класс функции. Вот 9X_typescript-types безумный пример:
declare function filterShape<
TShapes,
TShape extends Partial
>(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(), RedShape);
type isItRed = typeof amIRed;
/* Same as:
type isItRed = RedShape
*/
Проблема в том, что вы не 9X_discriminated-union можете получить значение color
. Вы можете RedShape.prototype.color
, но 9X_typescript это всегда будет неопределенным, потому 9X_atscript что значение применяется только в конструкторе. RedShape
скомпилирован 9X_atscript в:
var RedShape = /** @class */ (function () {
function RedShape() {
}
return RedShape;
}());
И даже если вы это сделаете:
class RedShape {
color: 'Red' = 'Red';
}
Это компилируется 9X_typescript в:
var RedShape = /** @class */ (function () {
function RedShape() {
this.color = 'Red';
}
return RedShape;
}());
И в вашем реальном примере конструкторы 9X_discriminated-union могут иметь несколько параметров и т. д., поэтому 9X_typescript создание экземпляра тоже может быть невозможно. Не 9X_vanilla-typescript говоря уже о том, что это не работает и 9X_vanilla-typescript с интерфейсами.
Возможно, вам придется вернуться 9X_typescript-typings к глупому способу, например:
class Action1 { type: '1' }
class Action2 { type: '2' }
type Actions = Action1 | Action2;
declare function ofType(
actions: TActions[],
action: new(...any) => TAction, type: TAction['type']): TAction;
const one = ofType(new Array(), Action1, '1');
/* Same as if
var one: Action1 = ...
*/
Или в вашей 9X_typescript-typings формулировке doSomething
:
declare function doSomething(
action: new(...any) => TAction, type: TAction['type']): TAction;
const one = doSomething(Action1, '1');
/* Same as if
const one : Action1 = ...
*/
Как упоминалось в комментарии 9X_discriminated-union к другому ответу, в TypeScript уже есть 9X_discriminated-union проблема для устранения проблемы вывода. Я 9X_atscript написал комментарий со ссылкой на объяснение 9X_vanilla-typescript этого ответа и предоставил более высокоуровневый 9X_discriminated-union пример проблемы here.
3
U
Uzbechonk
- Отличное разъ ...
Ответ #4
Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
К сожалению, вы не можете добиться этого, используя 9X_typescript-typings тип объединения (т. е. type MyActions = ExampleAction | AnotherAction;
).
Однако ваше решение 9X_discriminated-union отличное. Вам просто нужно использовать 9X_typescript этот способ для определения нужного вам 9X_typescript-typings типа.
const result2 = items.doSomething('Example');
Хотя вам это не нравится, это кажется 9X_typescript вполне законным способом делать то, что 9X_atscript вы хотите.
2
C
CCcatch
Ответ #5
Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript
Немного подробнее о настройке, но мы можем 9X_atscript достичь желаемого 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 {
doSomething(key: K): T[K];
}
const items = new Example();
const result1 = items.doSomething('Example');
console.log(result1.example);
1
F
FoMMkA
-
1
-
2
-
1
-
12
-
3
-
1
-
2
-
2
-
3
-
9
-
4
-
4
-
2
-
2
-
4
-
6
-
2
-
2
-
1
-
1
-
2
-
3
-
3
-
2
-
2
-
8
-
2
-
4
-
4
-
9
-
1
-
2
-
16
-
1
-
2
-
3
-
14
-
1
-
7
-
3
-
2
-
1
-
1
-
5
-
2
-
2
-
4
-
6
-
5
-
5