Typescript утверждает значение поля

#typescript #typescript-typings

#typescript #typescript-типизации

Вопрос:

В настоящее время я использую https://github.com/stephenh/ts-proto для генерации типов TypeScript из сообщений protobuf с использованием объединений типов для oneOf полей.

Тип образца может быть показан следующим образом:

 export interface Milestone {
  milestoneLabel?: 
     { $case: 'milestoneType', milestoneType: MilestoneType } |
     { $case: 'customMilestone', customMilestone: CustomMilestone };
  targetDate: Date | undefined;
}
  

Теперь, позже в коде, я хочу иметь функцию, подобную этой:

 export function getCustomMilestones(milestones: Milestone[]): Milestone[] {
  return milestones.filter(milestone => milestone.milestoneLabel?.$case === 'customMilestone');
}
  

Проблема в том, что сервер / компилятор языка TypeScript не понимает, что возвращаемый тип этой функции имеет фильтр для объединения типов, поэтому мне нужно снова утверждать $case при попытке прочитать это поле.

Есть ли способ сообщить компилятору, что выходные данные функции имеют определенный набор типов?

Ответ №1:

Используйте определяемый пользователем тип защиты:

 interface CustomMilestoneWrapper {
  milestoneLabel: { $case: 'customMilestone', customMilestone: CustomMilestone };
  targetDate: Date | undefined;
}

function isCustomMilestoneWrapper(milestone: Milestone): milestone is CustomMilestoneWrapper {
    return milestone.milestoneLabel?.$case === 'customMilestone'
}

function getCustomMilestones(milestones: Milestone[]): CustomMilestoneWrapper[] {
  return milestones.filter(isCustomMilestoneWrapper);
}
  

Ответ №2:

Хотя ответ Лесиака правильный, я хотел бы расширить: вы также можете использовать as для «приведения» к определенному типу. Пока вы уверены, что ваше приведение является надежным, это так же хорошо, как обычная защита типа.

Пример (игровая площадка):

 function filterMilestoneCustom(arg: Milestone[]): Array<Milestone amp; {milestoneLabel: { $case: 'customMilestone' }}> {
    return arg.filter(milestone => milestone.milestoneLabel?.$case === 'customMilestone') as any;
}

let milestones: Milestone[] = [];

for (let x of filterMilestoneCustom(milestones)) {
    let y: CustomMilestone = x.milestoneLabel.customMilestone;
}
  

Обратите внимание, что нет необходимости объявлять вспомогательный интерфейс. Тип Milestone amp; {milestoneLabel: { $case: 'customMilestone' }} правильно распознается компилятором.

Недостатком является то, что вам приходится писать такую функцию для каждого oneof случая. Проблема могла бы быть решена общим способом, если бы ts-proto не сделал oneof свойство необязательным.

oneof Тип объединения в ts-proto основан на моем предложении. С тех пор я написал плагин protobuf, который также использует типы объединения для oneof . Но он использует undefined для случая, когда ничего не выбрано. Сгенерированный интерфейс выглядит (в основном) следующим образом:

 export interface Milestone {
    milestoneLabel:
        { $case: 'milestoneType', milestoneType: MilestoneType } |
        { $case: 'customMilestone', customMilestone: CustomMilestone } |
        { $case: undefined };
    targetDate: Date | undefined;
}
  

Это позволяет написать следующую функцию защиты типа и фильтрации:

 type MilestoneCase<C extends Milestone["milestoneLabel"]["$case"]> = Milestone amp; {milestoneLabel: {$case: C}};

function isMilestoneCase<C extends Milestone["milestoneLabel"]["$case"]>($case: C, milestone: Milestone): milestone is MilestoneCase<C> {
    return milestone.milestoneLabel.$case === $case;
}

function filterMilestoneCase<C extends Milestone["milestoneLabel"]["$case"]>(milestones: Milestone[], $case: C): MilestoneCase<C>[] {
    const is = (ms: Milestone): ms is MilestoneCase<C> => isMilestoneCase($case, ms);
    return milestones.filter(is);
}
  

Которое можно использовать следующим образом (игровая площадка):

 let milestones: Milestone[] = [];

let customMilestones = filterMilestoneCase(milestones, "customMilestone");
for (let x of customMilestones) {
    let y: CustomMilestone = x.milestoneLabel.customMilestone;
}

let milestoneTypes = filterMilestoneCase(milestones, "milestoneType");
for (let x of milestoneTypes) {
    let y: MilestoneType = x.milestoneLabel.milestoneType;
}

let labelUndefined = filterMilestoneCase(milestones, undefined);
for (let x of labelUndefined) {
    let y: undefined = x.milestoneLabel.$case;
}
  

Если вы добавите регистр в свой oneof , вам не нужно писать новую функцию.

Смотрите protobuf-ts для плагина. Основное преимущество по сравнению с ts-proto, вероятно, заключается в том, что он написан полностью с нуля и не нуждается protobuf.js или Long.js . Размер кода для веб-приложений значительно меньше, чем в ts-proto, и вы можете делать некоторые интересные вещи с отражением и пользовательскими параметрами.

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

1. Очень приятно!! Мне нравится то, что вы сделали с вашей реализацией oneOf! Ваш фрагмент кода очень соответствует тому, о чем я думал. Спасибо за ответ!