Использовать имя типа для индексатора в интерфейсе - PullRequest
0 голосов
/ 30 октября 2018

Проблема в аннотации

У меня есть 2 группы типов, которые я хотел бы связать друг с другом.

// Group A
interface Hello { ... }
interface Foo { ... }

// Group B
interface World { ... }
interface Bar { ... }

Для этого я создаю третий интерфейс, который выполняет функцию поиска из одного типа в другой:

interface LookupMap {
  Hello: World;
  Foo: Bar;
}

В конечном итоге я бы хотел иметь возможность переходить от одного типа к другому, используя один интерфейс в качестве индексатора для LookupMap (аналогично тому, как строки и числа могут использоваться для поиска типов в литералах индексированных объектов):

type AltType<T> = LookupMap<T>;

const altHello: AltType<Hello> = ...; // should be of type World
const altFoo: AltType<Foo> = ...;     // should be of type Bar

Это не работает - кажется, что типы не могут быть использованы в качестве таких индексаторов.


Фактический вариант использования

Я пытаюсь добавить лучший способ ввода в Immutable.js.

Ниже этой точки есть какой-то мрачно выглядящий код. Если у вас уже есть решение, у вас все в порядке, не читая всего этого ...

Неизменяемые объекты имеют много полезных функций, для удобства давайте просто попробуем добавить типизацию к Map.get.

Пока все ваши значения примитивны, на самом деле это довольно просто:

interface Immutalizer<MUTABLE_TYPE> extends Immutable.Map<keyof MUTABLE_TYPE, MUTABLE_TYPE[keyof MUTABLE_TYPE]> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: MUTABLE_TYPE[PROP_NAME]): MUTABLE_TYPE[PROP_NAME];
}

interface MyInterface {
  hello: boolean;
  world: string;
  foo: number;
  bar: symbol;
}

interface MyImmutableInterface extends Immutalizer<MyInterface> {};

const myObject: MyImmutableInterface = Immutable.fromJS(...);
myObject.get("hello"); // boolean
myObject.get("world"); // string
myObject.get("foo");   // number
myObject.get("bar");   // symbol

Погружаясь глубже, если некоторые из наших реквизитов являются сложными объектами, мы должны предоставить Immutalizer второй тип, чтобы придать ему некоторый контекст:

interface Immutalizer<
  MUTABLE_TYPE,
  COMPLEX_OBJECT_KEYMAP extends { [PROP_NAME in keyof MUTABLE_TYPE]: any } = MUTABLE_TYPE
> extends Immutable.Map<
  keyof MUTABLE_TYPE,
  COMPLEX_OBJECT_KEYMAP[keyof MUTABLE_TYPE]
> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: COMPLEX_OBJECT_KEYMAP[PROP_NAME]): COMPLEX_OBJECT_KEYMAP[PROP_NAME];
}


interface Hello {
  foo: string;
  bar: World;
}

interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello, {
  foo: string;
  bar: ImmutableWorld;
}> {};

interface ImmutableWorld extends Immutalizer<World> {}; // this one is all primitives so the default type will cover it

const myObject: ImmutableHello = Immutable.fromJS(...);
myObject.get("bar");          // ImmutableWorld
myObject.get("foo");          // string
myObject.get("bar").get("a"); // number
myObject.get("bar").get("b"); // symbol

Это МНОГО занятой работы, и она становится только хуже, чем глубже ваше дерево объектов - так что я разработал альтернативное решение, которое просто о, так близко к работе, но не совсем там еще:

type Primitive = string | number | boolean | symbol | String | Number | Boolean | Symbol;
type PrimitiveSwitch<PROP_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> =
  PROP_TYPE extends Primitive ?
  PROP_TYPE :
  IMMUTALIZER_MAP[PROP_TYPE]; // TYPE ERROR: Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.
interface ImmutalizerMap { [mutableType: string]: Immutalizer<any, this> }

interface Immutalizer<MUTABLE_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(
    prop: PROP_NAME,
    notSetValue?: PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>
  ): PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>;
}


export interface Hello {
  foo: string;
  bar: World;
}

export interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello, ImmutalizerMap> { }
interface ImmutableWorld extends Immutalizer<World, ImmutalizerMap> { }

interface MyImmutalizerMap {
  Hello: ImmutableHello;
  World: ImmutableWorld;
}

const hello: ImmutableHello = Immutable.fromJS(...);

hello.get("foo"); // string
hello.get("bar"); // unknown (should be ImmutableWorld)

Бит Immutalizer сам по себе немного грубоват, но его использование теперь (теоретический) бриз:

  • Поддерживать отображение всех типов и связанных с ними неизменяемых типов.
  • Формируйте неизменяемые типы, используя Immutalizer, передавая базовый тип и ImmutalizerMap, которому он принадлежит
  • Immutalizer делает все остальное, используя PrimitiveSwitch, чтобы определить, нужно ли искать данный тип в ImmutalizerMap или нет.

Но, как указано в абстрактной версии выше, доступ к IMMUTALIZER_MAP[PROP_TYPE] в PrimitiveSwitch вызывает ошибку типа: Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.


Вопрос

Можно ли использовать интерфейс (имя) в качестве индексаторов в других интерфейсах? Есть ли лучшее решение для неизменной типизации?

1 Ответ

0 голосов
/ 30 октября 2018

Ну, не ответ на вопрос об индексаторах, я нашел более полное решение для добавления набора текста в Immutable.js с помощью рекурсии:

type Primitive = string | number | boolean | symbol | String | Number | Boolean | Symbol;

type PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME extends keyof MUTABLE_TYPE> = MUTABLE_TYPE[PROP_NAME] extends Primitive ? MUTABLE_TYPE[PROP_NAME] : Immutalizer<MUTABLE_TYPE[PROP_NAME]>

interface Immutalizer<MUTABLE_TYPE> extends Immutable.Map<keyof MUTABLE_TYPE, Immutalizer<MUTABLE_TYPE[keyof MUTABLE_TYPE]> | MUTABLE_TYPE[keyof MUTABLE_TYPE]> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME>): PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME>
}

interface Hello {
  foo: string;
  bar: World;
}

interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello> { };
interface ImmutableWorld extends Immutalizer<World> { };

let hello: ImmutableHello = Immutable.fromJS({});
let world: ImmutableWorld = hello.get("bar");

hello.get("bar").get("b"); // symbol
world.get("b");            // symbol

Вы теряете имена интерфейсов (hello.get("bar") возвращает Immutalizer<World> вместо ImmutableWorld), но типы по-прежнему совместимы.

...