Сужение возвращаемого типа от универсального размеченного объединения в 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 я мог адаптировать здесь что-то подобное.

32
0
5
Общее количество ответов: 5

Ответ #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
3

  • Есть ли причина, по к ...

Ответ #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
0

Ответ #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
1

  • Отличное разъ ...

Ответ #4

Ответ на вопрос: Сужение возвращаемого типа от универсального размеченного объединения в TypeScript

К сожалению, вы не можете добиться этого, используя 9X_typescript-typings тип объединения (т. е. type MyActions = ExampleAction | AnotherAction;).

If we have a value that has a union type, we can only access members that are common to all types in the union.

Однако ваше решение 9X_discriminated-union отличное. Вам просто нужно использовать 9X_typescript этот способ для определения нужного вам 9X_typescript-typings типа.

const result2 = items.doSomething('Example');

Хотя вам это не нравится, это кажется 9X_typescript вполне законным способом делать то, что 9X_atscript вы хотите.

2
0

Ответ #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
0