Выведите тип свойства класса на основе другого метода класса, возвращающего условный тип

Я пытаюсь определить тип свойства класса на основе условного типа метода, возвращаемого из другого класса.

Свойство класса Parent назначается внутри метода класса Builder, оно и должно быть таким.

Код должен быть таким. Реальный сценарий намного сложнее с декораторами и большим количеством зависимостей, но в конечном итоге он работает следующим образом:

class ClassA {
  propString: string
}

class ClassB {
  propNumber: number
}

type TypeAConstructor = new () => ClassA
type TypeBConstructor = new () => ClassB

class Builder {
  body: ClassA | ClassB

  build<T extends TypeAConstructor | TypeBConstructor>(Ctr: T, parent: Parent): T extends TypeAConstructor ? ClassA : ClassB {
    // I have to instantiate in this way, the real scenario is much more complex, but at the end it works like this
    if (new Ctr() instanceof ClassA) {
      this.body = new ClassA()
    } else {
      this.body = new ClassB()
    }
    // Parent obj will be assigned from this method
    parent.obj = this.body

    // Return type is correct
    return this.body as any // If I don't set as any, typescript throws an error
  }
}

class Parent {
  obj: ClassA | ClassB // ** Type should be inferred here. How to do it? **
  builder: Builder = new Builder()

  constructor() {
    const body = this.builder.build(ClassA, this)
    // body type is correct, it is ClassA
    // this.obj has no type, and it should be ClassA
  }
}

Это невозможно. Если build() изменяет один из своих аргументов, то TS не может разумно изменить тип этого аргумента. Вы могли бы почти использовать функции утверждения, но это не позволит определить тип поля obj. Если вы можете изменить build() так, чтобы он просто возвращал объект, а не что-либо мутировал, как показано в этой ссылке на игровую площадку, тогда это будет работать. Но поскольку вы несколько раз сказали «так должно быть», то ответ «тогда это невозможно». Это полностью решает вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?

jcalz 26.06.2024 01:27

@jcalz ответ правильный. Давай и спасибо.

Khanon 26.06.2024 05:37
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
2
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как и просили, это невозможно.

Единственная возможность, которую TypeScript имеет для определения типа поля класса , — это явная инициализация поля или явное назначение его в конструкторе. Но внутри конструктора Parent вы только неявно присваиваете свойство obj, вызывая другой метод. TypeScript не отслеживает такие вещи за пределами границ функций или методов.

Ну, вы могли бы превратить Builderbuild() в функцию утверждения, которая сужает свой второй аргумент, но это вообще не взаимодействует с инициализацией поля класса. Так что это не работает. Если вам необходимо разрешить build() изменять один из своих аргументов, вам придется просто обойти это, вручную аннотируя и утверждая типы:

class Parent {
  obj!: ClassA; // can't infer
  builder: Builder = new Builder()

  constructor() {
    const body = this.builder.build(ClassA, this)
  }
}

Но я настоятельно рекомендую не вносить изменения, а просто выполнить инициализацию «обычным» способом:

class Builder {

    body?: ClassA | ClassB

    build<T extends TypeAConstructor | TypeBConstructor>(Ctr: T):
        T extends TypeAConstructor ? ClassA : ClassB {
            ⋯
    }
}

class Parent {
    obj; // now inferred as ClassA
    builder: Builder = new Builder()
    constructor() {
        this.obj = this.builder.build(ClassA) // assigned
    }
}

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

Детская площадка, ссылка на код

Спасибо jcalz за подробный ответ. Основная проблема заключается в реальном коде: Parent ничего не выполняет, поэтому он не может назначить obj, build() выполняется из пользовательского метода класса-декоратора, непрозрачного для Parent, поэтому obj назначается в Builder. Как вы упомянули, сделать это невозможно, поскольку пользователь может выполнить build() в любой момент с любым значением. В этом случае я буду использовать дженерики, чтобы пользователь мог явно указать, какой тип будет возвращать build() в реализации класса декоратора.

Khanon 26.06.2024 17:54

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