Ты просил об этом.
Давайте сделаем некоторые манипуляции с типами, чтобы определить, является ли данный тип объединением или нет. Это работает с использованием распределительное свойство условных типов распространять объединение на составляющие, а затем замечать, что каждая составляющая уже, чем союз. Если это не так, то это потому, что у союза есть только одна составляющая (так что это не союз):
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
ComboObject
(ровно один дополнительный ключ со строковым свойством и никакими другими свойствами), но это ужасно и не то, что вы хотели бы использовать в любом производственном коде. Если вам интересно, я могу опубликовать это, но я думаю, что вы, возможно, захотите вместо этого заняться другими, более дружественными к TypeScript вариантами. - person jcalz   schedule 23.04.2018