У меня есть набор функций защиты типа с подписью:
type Validator<T> = (doc: any) => doc is T;
Я хочу иметь возможность составлять эти валидаторы. Например:
export const union = <T>(validators: Validator<>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
export const intersection = <T>(validators: Validator<>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
}
}
Тем не менее, я действительно не знаю, как ввести параметр validators
так, чтобы все, что там находится, было либо «в» T, либо «суммировалось» с T. Например, надеюсь, что следующее должно работать:
interface FooOne {
a: string;
}
interface FooTwo {
b: string;
}
interface FooThree {
c: string
}
type Bar = FooOne | FooTwo
type Baz = FooOne & FooTwo
const oneValidator = validator<FooOne>()
const twoValidator = validator<FooTwo>()
const threeValidator = validator<FooThree>()
const barValidator = union<Bar>([oneValidator, twoValidator]) // should succeed
const barValidator = union<Bar>([oneValidator]) // should succeed because FooOne is sufficient to validate Bar
const barValidator = union<Bar>([oneValidator, twoValidator, threeValidator]) // should fail because FooThree is not in Bar
const bazValidator = intersection<Baz>([oneValidator, twoValidator]) // should succeed
const bazValidator = intersection<Baz>([oneValidator]) // should fail because validating FooOne is insufficient to validate Baz
const bazValidator = intersection<Baz>([oneValidator, twoValidator, threeValidator]) // should fail because FooThree is not in Baz
Как я могу настроить типы, чтобы компилятор машинописного текста был достаточно умен, чтобы оценить эти композиции?
Например, может быть эта ссылка на игровую площадку демонстрирует то, что вы пытаетесь сделать, но мне пришлось сделать кучу догадок. Я правильно угадал? Если это так, я мог бы написать ответ, объясняющий; если нет, то что мне не хватает?
Я немного отредактировал код, но прошу прощения, что это не настоящий код — я не знаю, как это выразить. Под fail
я имею в виду, что TS должен выдавать ошибку типа — он должен знать, что даже если мы успешно проверим threeValidator
, если мы потерпим неудачу на oneValidator
и twoValidator
, вся функция не является допустимой защитой типа Bar. Точно так же для Baz, если мы успешно проверим oneValidator
, но не пройдем другие, TS должен выдать ошибку типа, потому что документ, который проходит только oneValidator
, может не относиться к типу Baz.
Вы посмотрели ссылку на игровую площадку выше или эту? Насколько я знаю, я реализую то, что вы хотите, за исключением: • вы предполагаете, что функция будет иметь аргумент типа, соответствующий проверенному типу, что я сомневаюсь в том, что это ваше фактическое требование; Разве недостаточно просто посмотреть, можно ли присвоить значение, выходящее из функции, тому типу, который вы хотите? (см. следующий комментарий)
(см. предыдущий комментарий) • кажется, вы хотите, чтобы Validator<Baz & Foo3>
не было Validator<Baz>
, что противоречит... по той же причине, по которой Validator<X>
также является Validator<X|Y>
, а Validator<X&Y>
также является Validator<X>
); Я не могу представить себе способ получить заданное вами поведение «годен/не годен» без серьезного злоупотребления системой типов. Могу ли я опубликовать свой подход в качестве ответа или есть какая-то веская причина, по которой он не соответствует вашим потребностям?
Я думаю, что вторая площадка правильная, спасибо! Можете ли вы объяснить, почему объединение возвращает T[число]?
Я объясню, когда напишу ответ, который должен быть сегодня
Я бы хотел сделать union
и intersection
универсальными в типе кортежа T
, соответствующему типу, который каждый validators
является охраняющим. Например, если у вас есть валидаторы vx
типа Validator<X>
, vy
типа Validator<Y>
и vz
типа Validator<Z>
, то при вызове union([vx, vy, vz])
мы хотим, чтобы T
было [X, Y, Z]
.
Затем мы можем представить ввод validators
как простой отображаемый тип в кортеже , например {[I in keyof T]: Validator<T[I]>}
. А чтобы намекнуть, что мы хотим, чтобы T
был кортежем, а не неупорядоченным типом массива , мы можем записать его как вариативный тип кортежа , [...{ I in keyof T]: Validator<T[I]> }]
. Поскольку отображаемый тип является гомоморфным (см. Что означает «гомоморфный отображаемый тип»?), компилятор может сделать вывод T
из значения этого отображаемого типа.
Это означает, что все, о чем нам нужно беспокоиться, это возвращаемые типы union
и intersection
.
Для union
возникает вопрос: «данный тип кортежа T
, как нам записать тип, который является объединением всех его типов элементов»? Это относительно легко написать; все, что нам нужно сделать, это индексировать в тип с number
, поскольку типы кортежей уже имеют number
сигнатуру индекса с объединением типов элементов. (Надеюсь, это имеет смысл; если у вас есть значение a
типа [string, number, boolean]
и вы индексируете его с помощью number
i
, которое не выходит за пределы, то тип a[i]
будет string | number | boolean
):
const union = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<T[number]> => {
return (doc: any): doc is T[number] => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
И давайте протестируем:
declare const oneValidator: Validator<FooOne>;
declare const twoValidator: Validator<FooTwo>;
declare const threeValidator: Validator<FooThree>;
const uv1 = union([oneValidator]);
// const uv1: Validator<FooOne>
const uv12 = union([oneValidator, twoValidator]);
// const uv12: Validator<FooOne | FooTwo>
const uv123 = union([oneValidator, twoValidator, threeValidator]);
// const uv123: Validator<FooOne | FooTwo | FooThree>
Выглядит неплохо.
Для intersection
возникает вопрос: «данный тип кортежа T
, как нам записать тип, который является пересечением всех его типов элементов»? Это более вовлечено. Нет простого способа получить это... индексированные типы доступа соответствуют тому, что вы получаете, когда читаете свойства, а не записываете их.
Чтобы превратить кортеж в пересечение всех его типов элементов, нам нужно написать собственный служебный тип, который сопоставляет кортеж с версией с элементами в позиции контравариантного типа (см. Разница между дисперсией, ковариантностью, контравариантностью и Бивариантность в TypeScript ), а затем использовать условный вывод типов через infer, чтобы вывести один тип для тех, которые станут пересечением, как описано в документации.
Это выглядит так:
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer R) => void ? R : never;
который вы можете проверить, работает по назначению:
type Test = TupleToIntersection<[{ a: string }, { b: number }, { c: boolean }]>
// type Test = { a: string; } & { b: number; } & { c: boolean; }
И таким образом intersection
выглядит
const intersection = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<TupleToIntersection<T>> => {
return ((doc: any): doc is TupleToIntersection<T> => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
});
}
И давайте протестируем:
const iv1 = intersection([oneValidator]);
// const iv1: Validator<FooOne>
const iv12 = intersection([oneValidator, twoValidator]);
// const iv12: Validator<FooOne & FooTwo>
const iv123 = intersection([oneValidator, twoValidator, threeValidator]);
// const iv123: Validator<FooOne & FooTwo & FooThree>
Также хорошо выглядит.
Я бы хотел, чтобы это был не псевдокод; что-то из этого довольно очевидно, что оно должно означать (например,
type Validator<T> = (doc: any): doc is T;
должно бытьtype Validator<T> = (doc: any) => doc is T;
), но кое-что я не понимаю (например,const barValidator = union([oneValidator, twoValidator, threeValidator]) // should fail because FooThree is not in Bar
, что вы имеете в виду, что здесь должно «не сработать»? делать сBar
? Это потому, что имя переменной начинается с"bar"
? ). Не могли бы вы отредактировать код, чтобы он был минимально воспроизводимым примером?