Возможно ли иметь общие декораторы в TypeScript, которые можно было бы связать в цепочку в соответствии с их типами ввода/вывода?

Для некоторых наших интеграций у нас есть довольно «шаблонные» реализации в нашей кодовой базе, которые можно удобно поместить в шаблон «канал и фильтр», ИМХО.

«Компоненты» могут выглядеть следующим образом:

class Component1<In, Out, Xin, Xout>
class Component2<Xin, Xout, Yin, Yout>
class Component3<Yin, Yout> // only has 2 params but could be <Yin, Yout, None, None> for a custom 'None' type

Идея состоит в том, чтобы иметь что-то, что позволит им быть «привязанными», чтобы сделать что-то вроде этого:

const c1 = new Component1<A,B,C,D>(...) //perhaps pass the param types in constructor? Other options?
const c2 = new Component2<C,D,E,F>(...)
const c3 = new Component3<E,F, None, None>(...)

const chain = c1.andThen(c2).andThen(c3) // The "last" item in the chain would "always" be a component of type <X,Y, None, None>

chain.run() // Not sure if this is needed but to make it clear that something "runs" this chain

Я не могу придумать какой-либо «общий» способ создания этих компонентов, где эта цепочка может быть «определена» во время компиляции, чтобы ограничить, какие компоненты могут быть связаны с другими (т. Е. Типы ввода/вывода должны совпадать). Таким образом, за c1 может следовать только c2, но не c3, но после c3 ничего не может быть сцеплено.

Это вообще возможно? Что-нибудь, чтобы получить это достаточно близко?

(Для любознательных: попытка добиться аналогичной «композитивности», которую Finagle предлагает в мире Scala)


person PhD    schedule 11.03.2019    source источник


Ответы (2)


Ваше использование дженериков немного сбивает меня с толку, так как не похоже, что вы прояснили разницу между параметром типа variables и конкретными типами, которые вы в них подключаете. Не говоря уже об использовании вами терминов, не относящихся к ТС, таких как val и None. Во всяком случае, следующий код компилируется и может дать вам то поведение, которое вы ищете:

type NotNever<T, Y=T, N=never> = [T] extends [never] ? N : Y;

// just create types, don't worry about implementation
declare class BaseComponent<In, Out, Xin=never, Xout=never> {
  // make BaseComponent depend structurally on type parameters
  i: In;
  o: Out;
  xi: Xin;
  xo: Xout;

  // andThen() is generic, and only accepts the right kind of other component
  // only callable if Xin and Xout are *not* never
  andThen<Yin, Yout>(
    this: NotNever<Xin | Xout, this>,
    c: BaseComponent<Xin, Xout, Yin, Yout>
  ): BaseComponent<In, Out, Yin, Yout>;

  // run() is only callable if Xin and Xout *are* never
  run(this: BaseComponent<In, Out, never, never>): void;
}

// create some concrete subclasses where parameters are set with string literal types
class Component1 extends BaseComponent<'In', 'Out', 'Xin', 'Xout'> { }
class Component2 extends BaseComponent<'Xin', 'Xout', 'Yin', 'Yout'> { }
class Component3 extends BaseComponent<'Yin', 'Yout'> { }

Вы можете увидеть, как это работает:

const c1 = new Component1();
const c2 = new Component2();
const c3 = new Component3();

c1.andThen(c1); // error
c1.andThen(c2); // okay
c1.andThen(c3); // error
c1.run(); // error

c2.andThen(c1); // error
c2.andThen(c2); // error
c2.andThen(c3); // okay
c2.run(); // error

c3.andThen(c1); // error
c3.andThen(c2); // error
c3.andThen(c3); // error
c3.run(); // okay

const chain = c1.andThen(c2).andThen(c3) // BaseComponent<'In', 'Out', never, never>;
chain.run(); // okay

Я думаю, это похоже на то, что вы хотите? Надеюсь, это поможет; удачи!


EDIT: еще один способ сделать то же самое, но не беспокоясь о условные типы и полиморфный this выглядит следующим образом:

// one base class for the end of the chain
declare class EndComponent<In, Out> {
  i: In;
  o: Out;
  run(): void;
}

// another base class for intermediate parts of the chain
declare class PipeComponent<In, Out, Xin, Xout> {
  i: In;
  o: Out;
  xi: Xin;
  xo: Xout;
  // andThen() is overloaded 
  andThen<Yin, Yout>(
    c: PipeComponent<Xin, Xout, Yin, Yout>
  ): PipeComponent<In, Out, Yin, Yout>;
  andThen(c: EndComponent<Xin, Xout>): EndComponent<In, Out>;
}

class Component1 extends PipeComponent<'In', 'Out', 'Xin', 'Xout'> { }
class Component2 extends PipeComponent<'Xin', 'Xout', 'Yin', 'Yout'> { }
class Component3 extends EndComponent<'Yin', 'Yout'> { }

Остальные должны вести себя как прежде. Удачи еще раз!

person jcalz    schedule 11.03.2019
comment
Это интересно. Особенно с polymorphic this и conditional types. Случай с EDIT тоже довольно интригующий. С этим точно можно поиграть. None упоминался как пользовательский тип, не являющийся частью TS. Тоже исправил val на const - забыл об этом. Но спасибо за эти предложения и за то, что нашли время с вашей стороны, чтобы помочь. Это может просто работать :) Но почему строковые литералы в параметрах, а не фактические типы? @jcalz - person PhD; 11.03.2019
comment
Строковые литералы: нет причин, кроме удобства. Если у вас есть настоящие типы, вы сможете использовать их вместо них. - person jcalz; 12.03.2019
comment
вторая реализация, кажется, нуждается в методе run, нет? - person PhD; 13.03.2019

Вот что у меня есть:

class Component<T, U> {
    constructor(private t: T, private u: U) {}
    andThen<V>(component: Component<U, V>): Component<U, V> {
        // implement andThen
        return component;
    }
    static run<T>(component: Component<T, null>) {
        // implement run
    }
}

type A = 'a'; const a: A = 'a';
type B = 'b'; const b: B = 'b';
type C = 'c'; const c: C = 'c';

const c1 = new Component<A, B>(a, b);
const c2 = new Component<B, C>(b, c);
const c3 = new Component<C, null>(c, null);

c2.andThen(c1); // TS2345: A is not assignable to B
Component.run(c1.andThen(c2)); // TS2345: Component<B,C> not assignable to Component<B,null>
Component.run(c1.andThen(c2).andThen(c3));

Я упростил код: <Xin, Xout, Yin, Yout><T,U>, но его легко адаптировать.

Тип цепочки соответствует ожидаемому. Во время выполнения Component<...,X>.andThen(Component<Y,...>) определяется как недопустимый (первый TS2345).

Небольшой рефакторинг, это не сама цепочка (т.е. Component), которая вызывает .run - через полчаса я не смог найти способ определить, во время компиляции, а не во время выполнения, что .run был вызван Component<..., null> (т.е. последним компонент цепи).

Вместо этого я поместил run в качестве статического метода Component, и он принимает в качестве входных данных только последний компонент. Использование показано в последних двух строках

И последнее, но не менее важное: класс остается очень общим и полиморфным, так что многие компоненты могут быть объединены в цепочку!

(new Component<'a', 'b'>('a', 'b'))
.andThen(new Component<'b', 'c'>('b', 'c'))
.andThen(new Component<'c', 'd'>('c', 'd'))
.andThen(new Component<'d', 'e'>('d', 'e'))
.andThen(new Component<'e', 'f'>('e', 'f'))
.andThen(new Component<'f', 'g'>('f', 'g'))

Надеюсь, это то, что вы искали.

person Nino Filiu    schedule 11.03.2019