Гарантия того, что будущие свойства `интерфейса` не будут иметь значений определенного типа

#typescript #types

#typescript #типы

Вопрос:

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

Как я могу ограничить тип любых новых свойств, добавленных в interface ?

Например, вот как выглядит текущий интерфейс:

 interface TraceMetadata {
  foo: string;
  bar: number;
  time: Date;
}
  

Я хотел бы иметь гарантию на уровне типа, что кто-то этого не сделает:

 interface TraceMetadata {
  foo: string;
  bar: number;
  time: Date;
  userSecret: Secret; // bad!
}
  

Как я могу это сделать в TypeScript?

Ответ №1:

Насколько я знаю, в TypeScript нет никакого механизма, который предотвратил бы это напрямую. Если бы компилятор разрешил вам использовать объявления интерфейса с самоссылками, вы могли бы добиться этого, но, похоже, TS4.0 и выше не допускают этого (см. microsoft / TypeScript #40315 для получения дополнительной информации).

Вместо этого вы могли бы придумать псевдоним типа sentinel, который будет выдавать предупреждение компилятора тогда и только тогда, когда кто-то добавит свойство a Secret -valued в TraceMetadata интерфейс. Например:

 type VerifyExtends<T, U extends T> = void;
type ValueOf<T> = T[keyof T];
type TraceMetadataProperties = ValueOf<TraceMetadata>;
type AcceptableTraceMetadataProperties = Exclude<TraceMetadataProperties, Secret>;

/* NOTE WELL! */
// If there is an error in the next line of code, then someone has put a Secret
// value into TraceMetadata.  You should find out who and tell them to
// be ashamed of themselves.
type TraceMetadataHasNoSecrets = VerifyExtends<AcceptableTraceMetadataProperties,
  TraceMetadataProperties // <-- error here if someone has been naughty
>;
  

VerifyExtends<T, U> Тип позволит вам указать только a T и U введите where U extends T . Тип TraceMetadataProperties — это объединение всех типов свойств в TraceMetadata , в то время AcceptableTraceMetadataProperties как этот тип с удаленными Secret значениями. Если TraceMetadataProperties extends AcceptableTraceMetadataProperties , то все в порядке, так как каждое свойство TraceMetadata является приемлемым. В противном случае существует свойство TraceMetadata , которое неприемлемо, и вы получите сообщение об ошибке.

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


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

С этим определением нет предупреждений компилятора:

 interface TraceMetadata {
  foo: string;
  bar: number;
  time: Date;
}

type TraceMetadataHasNoSecrets = VerifyExtends<AcceptableTraceMetadataProperties,
  TraceMetadataProperties // okay
>;
  

Но с этим определением существует:

 interface TraceMetadata {
  foo: string;
  bar: number;
  time: Date;
  userSecret: Secret; // bad!
}

type TraceMetadataHasNoSecrets = VerifyExtends<AcceptableTraceMetadataProperties,
  TraceMetadataProperties // error!
//~~~~~~~~~~~~~~~~~~~~~~~ <--
// Type 'ValueOf<TraceMetadata>' does not satisfy the constraint 
// 'string | number | Date'.
>;
  

Надеюсь, в случае, когда на самом деле есть предупреждение компилятора, кто-нибудь посмотрит на ошибку типа sentinel и увидит error here if someone has been naughty someone has put a Secret value into TraceMetadata и сможет исправить проблему.

Игровая площадка ссылка на код

Комментарии:

1. Приятно! Есть ли способ расширить эту проверку, чтобы охватить случай, когда некоторые свойства сами могут быть объектами, и в этом случае нам нужно будет убедиться, что ни одно из их свойств не является секретным? Я поиграл с ContainsSecret проверкой типа, но она работает не так, как я ожидал: « type ContainsString<T> = T расширяет строку? T : (T расширяет {[k: string]: ContainsString<T>} ? T : никогда); const a: ContainsString<число> = 2; // это (неожиданно) проверяет тип «