Вывод типа члена класса на основе другого члена

У меня есть класс TypeScript Foo с fieldOne: number | string и fieldTwo, который будет типа Bar[], когда fieldOne — это number, и Baz[], когда fieldOne — это string. Оба поля предоставляются как параметры конструктора.

Большая часть функциональности класса основана на объединении Bar и Baz, но в одном месте мне нужно сузить тип до Bar или Baz. Есть ли способ использовать компилятор, чтобы обеспечить создание экземпляра Foo только с помощью number и Bar[] или string и Baz[]. Затем в классе я могу сузить тип fieldTwo до Bar[] или Baz[], когда мне нужно?

Я могу добиться эффекта с помощью наследования, как показано здесь.

interface Bar {
  foo: string;
  bar: number;
}

interface Baz {
  foo: string;
  baz: number;
}

abstract class Foo<T extends number | string, U extends Bar[] | Baz[]> {
    private fieldOne: T;
    protected fieldTwo: U;

    constructor(paramOne: T, paramTwo: U) {
        this.fieldOne = paramOne;
        this.fieldTwo = paramTwo;
    }
    
    generalFunction() {
        console.info(this.fieldTwo.map(x => x.foo).join());
        this.specificFunction();
    }
    
    protected abstract specificFunction(): void;
}

class FooBar extends Foo<number, Bar[]> {
    specificFunction() {
        console.info(this.fieldTwo.map(x => x.bar).join());
    }
}

class FooBaz extends Foo<string, Baz[]> {
    specificFunction() {
        console.info(this.fieldTwo.map(x => x.baz).join());
    }
}

но я это немного затянуто.

Я надеялся получить что-то более лаконичное с условными типами, как здесь

type FooContents<T extends number | string> = T extends number ? Bar : Baz;

class Foo<T extends number | string> {
  private fieldOne: T;
  private fieldTwo: FooContents<T>;

  constructor(param1: T, param2: FooContents<T>) {
    this.fieldOne = param1;
    this.fieldTwo = param2;
  }

  generalFunction() {
    console.info(this.fieldTwo.foo);
  }

  specificFunction() {
    // somehow narrow the type, maybe based on this.fieldOne?
    // if fieldOne is number do something with Bar[];
    // if fieldOne is string do something with Baz[];
  }
}

но я не могу заставить его работать так, как мне нужно.

Третий способ, здесь, объединяет два поля в один объект.

type FooParams = {one: number, two: Bar[]} | {one: string, two: Baz[]};
class Foo {
    params: FooParams;

    constructor(params: FooParams) {
      this.params = params;
    }
    
    generalFunction() {
        console.info(this.params.two.map(x => x.foo).join());
    }

    specificFunction() {
      if (isNumberyBary(this.params)) this.processBars(this.params.two);
      if (isStringyBazy(this.params)) this.processBazs(this.params.two);
    }

    processBars(bars: Bar[]) {
        console.info(bars.map(x => x.bar).join());
    }

    processBazs(bazs: Baz[]) {
        console.info(bazs.map(x => x.baz).join());
    }
}

function isNumberyBary(x: FooParams): x is {one: number, two: Bar[]} {
  return typeof x === 'number';
}

function isStringyBazy(x: FooParams): x is {one: string, two: Baz[]} {
  return typeof x === 'number';
}

но опять же, это более затянуто, чем я надеялся.

Не могли бы вы дать нам минимальный воспроизводимый пример, с которым мы могли бы поиграть? Сейчас это кажется словесным описанием кода, а не самого кода. И в заголовке все еще говорится об «условных типах».

jcalz 18.04.2024 20:42

(см. предыдущий комментарий, пожалуйста) Объявления классов не могут напрямую реализовывать объединения. Было бы намного проще, если бы Foo содержал союз, а не был союзом. Я был бы склонен либо использовать наследование с объявлениями классов, как показано здесь , либо обойти это, используя выражения классов и утверждения типов, как показано здесь . Ни один из способов не является особенно эргономичным. number просто не является дискриминантным типом. Вместо этого вы действительно хотите дискриминируемый профсоюз.

jcalz 18.04.2024 21:00

@jcalz Я добавил свои попытки. Я пошел по пути наследования, но пытался получить что-то более лаконичное с помощью одного класса, что побудило меня попробовать условные типы. Утверждения типов также являются хорошим подходом.

eoinmullan 18.04.2024 21:38

Да, абсолютно. Я опасался раздувать их слишком сильно, поскольку они довольно длинные. Я добавлю то, что, по моему мнению, является сутью, но да, не стесняйтесь редактировать наверняка.

eoinmullan 18.04.2024 21:53

И, чтобы быть уверенным, что мы на одной волне, я, вероятно, напишу ответ, в котором будет сказано: «Поддерживаемый способ сделать это — с помощью наследования; в противном случае вы захотите сделать свой класс объединением, которое не может поддерживаться напрямую». , и его можно эмулировать только с помощью утверждений типа», и я покажу примеры. Это полностью решает вопрос или чего-то не хватает?

jcalz 18.04.2024 21:55

Только что добавил третий подход, но, опять же, он не очень аккуратен. Да, мы на одной волне, такой ответ охватит все.

eoinmullan 18.04.2024 22:09
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
7
68
1
Перейти к ответу Данный вопрос помечен как решенный

Другие вопросы по теме