Есть ли способ ограничить тип объекта тем же, что и у другого при использовании различающихся объединений? - PullRequest
0 голосов
/ 26 апреля 2019

У меня есть два класса:

interface Num {
    v: number, 
    type: 'num'
}

interface Sum {
    left: Expr, 
    right: Expr,
    type: 'sum'
}

и тип:

type Expr = Sum | Num;

Я хочу проверить, равны ли два Expr, но с отдельной функцией (в отличие отк объектно-ориентированному подходу).

Я хотел бы написать что-то вроде:

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    switch(e1.type) {
        case 'num': return e1.v === e2.v;
        case 'sum': return isEqual(e1.left, e2.left) && isEqual(e1.right, e2.right);
    }
}

Это невозможно, поскольку внутри переключателя функция не знает тип e2.

Мне нужно написать так:

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    switch(e1.type) {
        case 'num': return e1.v === (e2 as Num).v;
        case 'sum': return isEqual(e1.left, (e2 as Sum).left) && isEqual(e1.right, (e2 as Sum).right);
    }
}

, что довольно грязно.

Есть ли способ заставить компилятор выводить типы e1 и e2 в функции isEqual (после начальной проверки) должны быть равны или, по крайней мере, иметь общий супертип без явного приведения?

Ответы [ 2 ]

1 голос
/ 26 апреля 2019

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

Я просто хочу добавить к этому вопросу, что проблема в том, что пара дискриминируемых объединений не рассматривается компилятором как сам дискриминируемый союз. По логике это так, где дискриминант теперь является произведением дискриминантов двух типов объединений (то есть набора всех пар дискриминантов; в вашем случае это будет {["num","num"], ["num","sum"], ["sum","num"], и ["sum","sum"]}), но компилятор не проходит такой анализ. Я предполагаю, что это будет слишком дорого для компилятора проверять такие вещи, так как обнаружение, когда дискриминируемые объединения используются согласованно, может занять некоторое время, которое обычно будет потрачено впустую.

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

type DiscriminatedUnionPair<
  U, K extends keyof U,
  V, L extends keyof V,
  M extends Record<keyof M, readonly [U[K], V[L]]>> = {
    [P in keyof M]: {
      kind: P,
      first: Extract<U, Record<K, M[P][0]>>,
      second: Extract<V, Record<L, M[P][1]>>
    }
  }[keyof M];

function discriminatedUnionPair<U, K extends keyof U,
  V, L extends keyof V,
  M extends Record<keyof M, readonly [U[K], V[L]]>
>(
  firstUnion: U,
  firstDiscriminantKey: K,
  secondUnion: V,
  secondDiscriminantKey: L,
  disciminantMapping: M
): DiscriminatedUnionPair<U, K, V, L, M> {
  const p = (Object.keys(disciminantMapping) as Array<keyof M>).find(p =>
    disciminantMapping[p][0] === firstUnion[firstDiscriminantKey] &&
    disciminantMapping[p][1] === secondUnion[secondDiscriminantKey]
  );
  if (typeof p === "undefined") throw new Error("WHAT, MAPPING IS BAD");
  return { kind: p, first: firstUnion, second: secondUnion } as any;
}

Этот код можно где-то выбросить в библиотеке. Тогда вы можете реализовать isEqual() без ошибок:

function isEqual(e1: Expr, e2: Expr): boolean {
  const m = {
    sumsum: ["sum", "sum"],
    sumnum: ["sum", "num"],
    numsum: ["num", "sum"],
    numnum: ["num", "num"]
  } as const;
  const e = discriminatedUnionPair(e1, "type", e2, "type", m)
  switch (e.kind) {
    case 'numsum': return false;
    case 'sumnum': return false;
    case 'numnum': return e.first.v === e.second.v;
    case 'sumsum': return isEqual(e.first.left, e.second.left) &&
      isEqual(e.first.right, e.second.right);
  }
}

Важной строкой является вызов discriminatedUnionPair(e1, "type", e2, "type", m), который создает новый элемент типа {kind: 'sumsum', first: Sum, second: Sum} | {kind: 'sumnum', first: Sum, second: Num} | {kind: 'numsum', first: Num, second: Sum} | {kind: 'numnum', first: Num, second: Num}, который TypeScript распознает как распознаваемое объединение так, как вы ожидаете, и код ведет себя так, как вы ожидаете. Вы хотите:

const n1: Num = { type: "num", v: 1 }
const n2: Num = { type: "num", v: 2 }
const s1: Sum = { type: "sum", left: n1, right: n2 }
const s2: Sum = { type: "sum", left: n2, right: n1 }

console.log(isEqual(n1, n1)); // true
console.log(isEqual(n1, n2)); // false
console.log(isEqual(n1, s1)); // false
console.log(isEqual(s2, n2)); // false
console.log(isEqual(s1, s2)); // false
console.log(isEqual(s2, s2)); // true

Вероятно, это того не стоит, поскольку вы заставляете существовать новый дискриминируемый союз во время выполнения, который имеет значение только во время компиляции. И вы вручную перечисляете все возможные дискриминантные пары, которые могут быстро состариться. Но я просто хотел показать, что это возможно.

?????

Надеюсь, это поможет; удачи!

1 голос
/ 26 апреля 2019

Сужать распознаваемый тип объединения все еще довольно глупо.Но да, есть способ, просто вам это, вероятно, не понравится.

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    const t1 = e1.type
    const t2 = e2.type
    switch(e1.type) {
        case 'num': return e1.type === e2.type && e1.v === e2.v;
        case 'sum': return e1.type === e2.type && isEqual(e1.left, e2.left) && isEqual(e1.right, e2.right);
    }
}

Чтобы сделать его немного лучше, используйте охрану пользовательского типа

const s = function isSameType<T>(e1: T, e2: any): e2 is typeof e1 {
    return true
}

// then replace `e1.type === e2.type` with `s(e1, e2)`
// ...
    case 'num': return s(e1, e2) && e1.v === e2.v;

IЯ бы кратко объяснил причину.

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

/* PSEUDO CODE */
e1.type !== e2.type => (typeof e1 == typeof e2)
case 'num' => (typeof e1 == Num)

// we as human can tell:
(typeof e1 == typeof e2) & (typeof e1 == Num)
=> typeof e2 == Num

// but TS still see `typeof e2 == Expr`
// not smart enough to make connection btw 2 inferences.

Однако, если вы поставите case перед ===, ТС может сказать

/* PSEUDO CODE */
case 'num' => (typeof e1 == Num)
e1.type === e2.type => (typeof e1 == typeof e2)
(typeof e1 == Num) & (typeof e1 == typeof e2)
=> typeof e2 == Num
...