Как набрать объект с известными и неизвестными ключами в TypeScript

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

interface ComboObject {
  known: boolean
  field: number
  [U: string]: string
}

const comboObject: ComboObject = {
  known: true
  field: 123
  unknownName: 'value'
}

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

Единственное решение, которое у меня есть до сих пор, - использовать подписи индекса и настроить тип объединения всех возможных типов:

interface ComboObject {
  [U: string]: boolean | number | string
}

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

Есть ли лучший подход? Может ли что-то с условными типами TypeScript 2.8 помочь?


person Jacob Gillespie    schedule 22.04.2018    source источник
comment
TypeScript не предназначен для этого. У меня есть способ заставить компилятор (используя условные типы) ограничить параметр функции типом, соответствующим вашему предполагаемому типу ComboObject (ровно один дополнительный ключ со строковым свойством и никакими другими свойствами), но это ужасно и не то, что вы хотели бы использовать в любом производственном коде. Если вам интересно, я могу опубликовать это, но я думаю, что вы, возможно, захотите вместо этого заняться другими, более дружественными к TypeScript вариантами.   -  person jcalz    schedule 23.04.2018
comment
@jcalz да, если бы вы могли опубликовать или иным образом отправить его, это было бы здорово, это могло бы вызвать некоторые идеи, даже если это не совсем работоспособно.   -  person Jacob Gillespie    schedule 23.04.2018


Ответы (2)


Ты просил об этом.

Давайте сделаем некоторые манипуляции с типами, чтобы определить, является ли данный тип объединением или нет. Это работает с использованием распределительное свойство условных типов распространять объединение на составляющие, а затем замечать, что каждая составляющая уже, чем союз. Если это не так, то это потому, что у союза есть только одна составляющая (так что это не союз):

type IsAUnion<T, Y = true, N = false, U = T> = U extends any
  ? ([T] extends [U] ? N : Y)
  : never;

Затем используйте его, чтобы определить, является ли данный тип string однострочным литералом (так: не string, не never и не объединение):

type IsASingleStringLiteral<
  T extends string,
  Y = true,
  N = false
> = string extends T ? N : [T] extends [never] ? N : IsAUnion<T, N, Y>;

Теперь мы можем заняться вашей конкретной проблемой. Определите BaseObject как часть ComboObject, которую вы можете определить прямо:

type BaseObject = { known: boolean, field: number };

И, готовясь к сообщениям об ошибках, давайте определим ProperComboObject, чтобы, когда вы ошиблись, ошибка давала намек на то, что вы должны были делать:

interface ProperComboObject extends BaseObject {
  '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!': string
}

А вот и основное блюдо. VerifyComboObject<C> принимает тип C и возвращает его нетронутым, если он соответствует вашему желаемому типу ComboObject; в противном случае он возвращает ProperComboObject (что также не соответствует) для ошибок.

type VerifyComboObject<
  C,
  X extends string = Extract<Exclude<keyof C, keyof BaseObject>, string>
> = C extends BaseObject & Record<X, string>
  ? IsASingleStringLiteral<X, C, ProperComboObject>
  : ProperComboObject;

Он работает путем разделения C на BaseObject и оставшиеся ключи X. Если C не совпадает с BaseObject & Record<X, string>, значит, вы потерпели неудачу, поскольку это означает, что либо это не BaseObject, либо с дополнительными свойствами, отличными от string. Затем он проверяет наличие ровно одного ключа, проверяя X с помощью IsASingleStringLiteral<X>.

Теперь мы создаем вспомогательную функцию, которая требует, чтобы входной параметр совпадал с VerifyComboObject<C>, и возвращает входные данные без изменений. Это позволяет вам своевременно обнаруживать ошибки, если вам просто нужен объект правильного типа. Или вы можете использовать подпись, чтобы ваши собственные функции требовали правильного типа:

const asComboObject = <C>(x: C & VerifyComboObject<C>): C => x;

Давайте проверим это:

const okayComboObject = asComboObject({
  known: true,
  field: 123,
  unknownName: 'value'
}); // okay

const wrongExtraKey = asComboObject({
  known: true,
  field: 123,
  unknownName: 3
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

const missingExtraKey = asComboObject({
  known: true,
  field: 123
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

const tooManyExtraKeys = asComboObject({
  known: true,
  field: 123,
  unknownName: 'value',
  anAdditionalName: 'value'
}); // error, '!!!ExactlyOneOtherStringPropertyNoMoreNoLess!!!' is missing

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

Вы можете увидеть код в действии в Игровая площадка.


Опять же, я не думаю, что рекомендую это для производственного кода. Мне нравится играть с системой шрифтов, но эта кажется особенно сложной и хрупкой, и я бы не стал ' я не хочу нести ответственность за любые непредвиденные последствия.

Надеюсь, это поможет тебе. Удачи!

person jcalz    schedule 23.04.2018
comment
Это здорово, спасибо! Это помогает понять, как работает система типов. И последний вопрос: может ли неизвестный ключ иметь тип интерфейса, а не строковый? Я бы предположил, что это возможно, изменив Record<X, string> на Record<X, InterfaceType>, но, похоже, это допускает любые произвольные свойства в интерфейсе. - person Jacob Gillespie; 23.04.2018
comment
Что вы имеете в виду под произвольными свойствами? Это не позволит вам не учитывать свойства или указывать им неправильные типы, не так ли? Если вы имеете в виду лишние свойства, да, это не удивительно. Проверка лишних свойств происходит только в определенных местах TypeScript. У него нет истинных точных типов, и принуждение компилятора отклонять лишние свойства привело бы к требуется еще один раунд типа гольф. - person jcalz; 24.04.2018
comment
Интересно, вы правы, он разрешает дополнительные свойства, но правильно проверяет типы данных свойств. Спасибо за ссылку на GitHub, которая также объясняет, как добиться такого же поведения. - person Jacob Gillespie; 24.04.2018
comment
Этот пример не работает с TypeScript 3.5.1 с ошибкой Тип 'keyof C' не может быть назначен типу 'string' - person Matthew Dean; 18.09.2019
comment
@jcalz Нет более простого способа сделать это в TypeScript в последней версии? Я действительно хочу что-то вроде: {foo: number; [key: notFoo]: string} - person Matthew Dean; 18.09.2019
comment
Если вы хотите, чтобы notFoo был однострочным ключом, то, вероятно, все равно, как я бы это сделал. Если вы просто хотите, чтобы notFoo был любым ключом или набором ключей, отличным от foo, вам все равно нужно использовать общий сопоставленный условный тип, но это немного проще. Пока и если TypeScript не реализует произвольные типы подписи индекса и отрицательные типы, нет конкретного способа сказать {foo: number; [k: string & not "foo"]: string}. - person jcalz; 18.09.2019

Хороший @jcalz

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

type BaseObject = { known: boolean, field: number };
type CoolType<C, X extends string | number | symbol = Exclude<keyof C, keyof BaseObject>> = BaseObject & Record<X, BaseObject>;
const asComboObject = <C>(x: C & CoolType<C>): C => x;

const tooManyExtraKeys = asComboObject({
     known: true,
     field: 123,
     unknownName: {
         known: false,
         field: 333
     },
     anAdditionalName: {
         known: true,
         field: 444
     },
});

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

ty

person hugo00    schedule 05.10.2018