Машинопись - постепенное расширение типа объекта

Я пытаюсь добиться с помощью TS следующего:

let m: Extendable
m.add('one', 1)
// m now has '.one' field
m.add('two', 2)
// 'm' now has '.one' and '.two' fields

Я знаком с возвратом расширенных типов в TS через:

function extend<T, V>(obj: T, val: V): T & {extra: V} {
    return {
        ...obj,
        extra: val
    }
}

В моем случае есть две проблемы:

1) объект m должен обновить свой тип после того, как add() был вызван, чтобы отразить добавление нового поля

2) имя нового поля параметризовано (не всегда extra, например)

Первую проблему можно решить, используя определение класса и каким-то образом используя утилиту TypeThis для повторной настройки типа, но мне не удалось найти достаточно документации о том, как ее использовать.

Любая помощь или руководство приветствуются. Спасибо!


person Abstract Algorithm    schedule 06.12.2019    source источник
comment
Справедливо предположение, что первым параметром add() является литерал, а не переменная.   -  person Abstract Algorithm    schedule 06.12.2019
comment
@ T.J.Crowder возвращающий экземпляр довольно прост. Я думаю, проблема в том, как отразить мутацию без оператора return.   -  person Maciej Sikora    schedule 06.12.2019
comment
Некоторые модификации вашей игровой площадки - typescriptlang.org/play/   -  person Maciej Sikora    schedule 06.12.2019
comment
@MaciejSikora - Я знал, что есть лучший способ сделать то небольшое, что я сделал. :-)   -  person T.J. Crowder    schedule 06.12.2019
comment
Приятно @MaciejSikora. Я пытаюсь найти ответ на этот stackoverflow.com/questions/50899400/ по мере обновления типа, но есть много других код, что становится очень трудно увидеть, что важно.   -  person Abstract Algorithm    schedule 06.12.2019
comment
Нет, этот ответ не имеет ничего общего с этой проблемой. В конструкторе есть переключатель, поэтому мы выполняем присваивание. После этого возвращаемый тип не изменится. В вашем типе вопроса меняется каждый вызов метода. Стандартный подход работает с использованием цепочки методов obj.add (). Add (). Add (), что позволяет сделать вывод о возврате. Но без использования return кажется невозможным, мы можем поиграть с охранниками типа, но для этого потребуется дополнительное условие. Постараюсь сделать что-нибудь из этого, но сегодня позже   -  person Maciej Sikora    schedule 07.12.2019
comment
Также есть функции утверждения в TS3.7 +, который может решить эту проблему, но есть досадная оговорка, связанная с аннотациями типов (let e = new Extendable() не будет работать, но let e: Extendable = new Extendable() будет), поэтому я не уверен, стоит ли оно того.   -  person jcalz    schedule 07.12.2019
comment
@AbstractAlgorithm вы все еще работаете с WebGL?   -  person gandra404    schedule 29.05.2020
comment
@ gandra404: да, при необходимости, но не ежедневно. зачем вообще спрашивать?   -  person Abstract Algorithm    schedule 04.06.2020


Ответы (1)


TypeScript 3.7 представил функции утверждения. который можно использовать для сужения типа передаваемых аргументов или даже this. Функции утверждения выглядят как охранники определяемых пользователем типов, но вы добавляете модификатор asserts перед предикатом типа. Вот как можно реализовать Extendable как класс с add() в качестве метода утверждения:

class Extendable {
    add<K extends PropertyKey, V>(key: K, val: V): asserts this is Record<K, V> {
        (this as unknown as Record<K, V>)[key] = val;
    }
}

Когда вы вызываете m.add(key, val), компилятор утверждает, что m будет иметь свойство с ключом с типом key и соответствующим значением с типом val. Вот как бы вы это использовали:

const m: Extendable = new Extendable();
//     ~~~~~~~~~~~~ <-- important annotation here!
m.add('one', 1)
m.add('two', 2)

console.log(m.one.toFixed(2)); // 1.00
console.log(m.two.toExponential(2)); // 2.00e+0

Все работает так, как вы ожидаете. После вызова m.add('one', 1) вы можете обратиться к m.one без предупреждения компилятора.

К сожалению, существует довольно серьезное предупреждение; функции утверждения работают, только если они имеют явно аннотированный тип. Согласно соответствующему запросу на вытягивание, «это конкретное правило существует для того, чтобы анализ потока управления потенциальных вызовы утверждений не запускают циклический дальнейший анализ ".

Это означает, что это ошибка:

const oops = new Extendable(); // no annotation
  oops.add("a", 123); // error!
//~~~~~~~~ <-- Assertions require every name in the call target to be declared with
// an explicit type annotation.

Единственное отличие состоит в том, что тип oops предполагается как Extendable вместо аннотированного как Extendable, как m. И вы получаете сообщение об ошибке при вызове oops.add(). В зависимости от вашего варианта использования это может быть либо несущественным, либо показательным.


Хорошо, надеюсь, что это поможет; удачи!

Ссылка на код

person jcalz    schedule 07.12.2019
comment
Это какое-то волшебство. Но он действительно достигает того, чего я хотел. Я не понимаю, почему он не утверждает Record<K, V> & {[key]: V} или что-то в этом роде, т.е. когда тип расширяется дополнительным полем? Я вижу, что он назначается позже, но когда изменится тип и как / почему? Кроме того, предостережение меня не беспокоит для моего варианта использования. - person Abstract Algorithm; 08.12.2019
comment
Я не уверен, что понимаю вопрос. Тип возврата add() as this is Record<K, V> означает, что если this.add() возвращается, то тип this будет сужен до Record<K, V>. Итак, m начинается как Extendable, и вы вызываете m.add("one",1), все, что находится после этой строки, будет иметь m как Extendable & Record<"one", number>. И после того, как вы вызовете m.add("two", 2), все, что находится после этой строки, будет иметь m как Extendable & Record<"one",number> & Record<"two", number>. Реализация add() должна фактически добавить поле, иначе утверждение будет ложью. - person jcalz; 08.12.2019
comment
Я понимаю, что вы написали, и в чем заключается логика, кроме места, где на самом деле происходит конкатенация двух типов (Extendable & {...} или Extendable & {...} & {...}. Assert только приводит / гарантирует, что это Record<K,V>, но не whatever is previous & Record<K,V>. Или, может быть, высказывание this is Record<K,V> делает это? - person Abstract Algorithm; 08.12.2019
comment
да, я думаю, что пересечение просто происходит автоматически в некоторых случаях. Если вас это беспокоит, вы можете сделать это this is this & Record<K, V>, чтобы он был явным. - person jcalz; 08.12.2019
comment
Да, понял. Я думаю, что asserts this is Record<K,V> не следует читать, поскольку this относится к типу Record<K,V>, но this удовлетворяет Record<K,V> для некоторой части this. Подобно тому, как обычно это делают охранники типов - они не совпадают в точности, но проверяют, какой интерфейс доступен. - person Abstract Algorithm; 08.12.2019
comment
@AbstractAlgorithm - Да, это не должно означать только это. Если у меня есть Foo, Bar extends Foo и const b: Bar, b будет Foo. Это не только Foo, но это Foo. - person T.J. Crowder; 08.12.2019