Я пытаюсь определить тип свойства класса на основе условного типа метода, возвращаемого из другого класса.
Свойство класса 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
}
}
@jcalz ответ правильный. Давай и спасибо.
Как и просили, это невозможно.
Единственная возможность, которую TypeScript имеет для определения типа поля класса , — это явная инициализация поля или явное назначение его в конструкторе. Но внутри конструктора Parent
вы только неявно присваиваете свойство obj
, вызывая другой метод. TypeScript не отслеживает такие вещи за пределами границ функций или методов.
Ну, вы могли бы превратить Builder
build()
в функцию утверждения, которая сужает свой второй аргумент, но это вообще не взаимодействует с инициализацией поля класса. Так что это не работает. Если вам необходимо разрешить 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() в реализации класса декоратора.
Это невозможно. Если
build()
изменяет один из своих аргументов, то TS не может разумно изменить тип этого аргумента. Вы могли бы почти использовать функции утверждения, но это не позволит определить тип поляobj
. Если вы можете изменитьbuild()
так, чтобы он просто возвращал объект, а не что-либо мутировал, как показано в этой ссылке на игровую площадку, тогда это будет работать. Но поскольку вы несколько раз сказали «так должно быть», то ответ «тогда это невозможно». Это полностью решает вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?