Создание защиты типа для интерфейса со всеми необязательными свойствами

У меня есть функция makeMergedState, которая принимает либо объект, либо массив типа ICustomState.

Функции содержат условные операторы в зависимости от того, является ли вход допустимым ICustomState или ICustomState []. В случае, если ввод является недопустимым объектом, ошибочно введенным, я хочу, чтобы функция выбрасывала.

Это тестовый пример, который я хочу добиться:

it("throws on invalid input", () => {
  expect(() => makeMergedState({ test: "" } as ICustomState)).toThrow();
});

ICustomState - это интерфейс TypeScript, содержащий только необязательные свойства. Я могу ввести охрану массива с помощью такой функции: const isCustomStateArray = (p: any): p is ICustomState[] => !!p[0];

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

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

Очень ценю любое предложение.

РЕДАКТИРОВАТЬ: пример Codesandbox


person eheu    schedule 17.01.2020    source источник
comment
Пожалуйста, рассмотрите возможность предоставления минимального воспроизводимого примера, как описано в Как спросить. В идеале что-то, что продемонстрировало бы вашу проблему при переносе в автономную среду IDE, например TypeScript Playground. Прямо сейчас я не знаю, что такое ICustomState, поэтому не знаю, что посоветовать. Удачи!   -  person jcalz    schedule 17.01.2020
comment
›ICustomState - это интерфейс TypeScript, содержащий только необязательные свойства.   -  person eheu    schedule 17.01.2020
comment
Обновлено, чтобы предоставить пример Codesandbox. Бонусные баллы за решение maintable, которое мне не придется расширять по мере изменения ICustomState.   -  person eheu    schedule 18.01.2020


Ответы (1)


Ответ на другой вопрос касается того, почему непросто автоматизировать защиту типов времени выполнения для интерфейсов времени компиляции (т. е. введите стирание) и какие у вас есть варианты (т. е. генерация кода как в typescript-is, классы и декораторы как в _ 2_ или объекты схемы, которые можно использовать для создания как защиты типов, так и интерфейсов, как в _ 3_).

Если это важно, я перевел пример кода из этого вопроса на ваш случай. Это один из возможных способов написания кода, который генерирует как защиту типа, так и интерфейсы. Ваша библиотека схем может выглядеть так:

namespace G {
  export type Guard<T> = (x: any) => x is T;
  export type Guarded<T extends Guard<any>> = T extends Guard<infer V> ? V : never;
  const primitiveGuard = <T>(typeOf: string) => (x: any): x is T => typeof x === typeOf;
  export const gString = primitiveGuard<string>("string");
  export const gNumber = primitiveGuard<number>("number");
  export const gBoolean = primitiveGuard<boolean>("boolean");
  export const gNull = (x: any): x is null => x === null;
  export const gObject =
    <T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
      (x: any): x is T => typeof x === "object" && x !== null &&
        (Object.keys(propGuardObj) as Array<keyof T>).
          every(k => (k in x) && propGuardObj[k](x[k]));
  export const gPartial =
    <T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
      (x: any): x is { [K in keyof T]?: T[K] } => typeof x === "object" && x !== null &&
        (Object.keys(propGuardObj) as Array<keyof T>).
          every(k => !(k in x) || typeof x[k] === "undefined" || propGuardObj[k](x[k]));
  export const gArray =
    <T>(elemGuard: Guard<T>) => (x: any): x is Array<T> => Array.isArray(x) &&
      x.every(el => elemGuard(el));
  export const gUnion = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
    (x: any): x is T | U => tGuard(x) || uGuard(x);
  export const gIntersection = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
    (x: any): x is T & U => tGuard(x) && uGuard(x);
}

Исходя из этого, мы можем построить вашу IExample1 гвардию и интерфейс:

const _isExample1 = G.gObject({
  a: G.gNumber,
  b: G.gNumber,
  c: G.gNumber
});
interface IExample1 extends G.Guarded<typeof _isExample1> { }
const isExample1: G.Guard<IExample1> = _isExample1;

Если вы посмотрите на _isExample1, вы увидите, что он похож на {a: number; b: number; c: number}, и если вы посмотрите IExample1, он будет иметь эти свойства. Обратите внимание, что gObject Guard не заботится о дополнительных свойствах. Значение {a: 1, b: 2, c: 3, d: 4} будет допустимым IExample1; это нормально, потому что типы объектов в TypeScript не являются точными. Если вы хотите, чтобы ваша защита типа принудительно обеспечивала отсутствие дополнительных свойств, вы должны изменить реализацию gObject (или сделать gExactObject или что-то в этом роде).

Затем мы создаем защиту и интерфейс ICustomState:

const _isCustomState = G.gPartial({
  example1: isExample1,
  e: G.gString,
  f: G.gBoolean
});
interface ICustomState extends G.Guarded<typeof _isCustomState> { }
const isCustomState: G.Guard<ICustomState> = _isCustomState;

Здесь мы используем gPartial, чтобы объект имел только необязательные свойства, как в вашем вопросе. Обратите внимание, что средство защиты gPartial проверяет объект-кандидат и отклоняет объект только в том случае, если присутствует ключ неправильного типа. Если ключ отсутствует или undefined, это нормально, поскольку именно это означает необязательное свойство. И, как gObject, gPartial не заботится о дополнительных свойствах.

Когда я смотрю на ваш кодcodeandbox, я вижу, что вы возвращаете true, если присутствует какой-либо из ключей свойств, и false в противном случае, но это неправильный тест. Объект {} без свойств может быть назначен типу объекта со всеми необязательными свойствами, поэтому вам не нужно присутствие каких-либо свойств. И наличие ключа само по себе не считается, поскольку объект {e: 1} не должен быть назначен {e?: string}. Вам необходимо проверить все свойства, присутствующие в объекте-кандидате, и отклонить его, если какое-либо из свойств имеет неправильный тип.

(Примечание: если у вас есть объект с некоторыми необязательными и некоторыми обязательными свойствами, вы можете использовать пересечение типа G.gIntersection(G.gObject({a: G.gString}), G.gObject({b: G.gNumber})), которое будет защищать {a: string} & {b?: number}, что совпадает с {a: string, b?: number}.)

Наконец ваш ICustomState[] охранник:

const isCustomStateArray = G.gArray(isCustomState);

Давайте протестируем этот CustomState охранник, чтобы увидеть, как он себя ведет:

function testCustomState(json: string) {
  console.log(
    json + " " + (isCustomState(JSON.parse(json)) ? "IS" : "is NOT") + " a CustomState"
  );
}
testCustomState(JSON.stringify({})); // IS a CustomState
testCustomState(JSON.stringify({ e: "" })); // IS a CustomState
testCustomState(JSON.stringify({ e: 1 })); // is NOT a CustomState
testCustomState(JSON.stringify({ example1: { a: 1, b: 2, c: 3 } })); // IS a CustomState
testCustomState(JSON.stringify({ w: "", f: true })); // IS a CustomState

Думаю, все в порядке. Единственный неудачный пример - {e:1}, потому что его свойство e имеет неправильный тип (number вместо string | undefined).


В любом случае, надеюсь, это поможет; удачи!

площадка ссылку на код

person jcalz    schedule 18.01.2020
comment
Впечатляющий ответ! Большое спасибо. Очень помогает. - person eheu; 20.01.2020
comment
Похоже, что правильное решение для меня - это выйти из моего CRA и использовать машинописный текст с плагином-преобразователем или KISS и провести рефакторинг моего метода. - person eheu; 20.01.2020