Это замечательный вопрос, на который было приятно ответить!
Как мы заставим компилятор отслеживать все аргументы, которые методы экземпляра класса получили за время существования экземпляра?
Вот так! Это большая просьба! Сначала я не был уверен, возможно ли это.
Вот что должен делать компилятор за время существования экземпляра класса:
- При каждом вызове метода добавляйте к набору аргументов, полученных экземпляром.
- Сгруппируйте эти аргументы, чтобы мы могли позже проверить их.
Здесь мы go ...
Ответ
Следующий подход достаточно сложно, что я предоставил только сигнатуры метода. Я также упростил эти подписи до минимальных требований, которые могут express идея. Реализации метода будут относительно простыми для вас.
Подход использует типы аккумуляторов для отслеживания типов аргументов. Эти типы аккумуляторов похожи на объекты-аккумуляторы, которые мы использовали бы в функции Array.reduce
.
Здесь ссылка на игровую площадку и код:
type TrackShapes<TSlideBuilder, TNextShape> =
TSlideBuilder extends SlideBuilder<infer TShapes, infer TAnimations>
? SlideBuilder<TShapes | TNextShape, TAnimations>
: never;
type TrackAnimations<TSlideBuilder, TNextAnimation> =
TSlideBuilder extends SlideBuilder<infer TShapes, infer TAnimations>
? SlideBuilder<TShapes, TAnimations | TNextAnimation>
: never;
export class SlideBuilder<TShape, TAnimation> {
public static start(): SlideBuilder<never, never> {
return new SlideBuilder<never, never>();
};
public withShape<TNext extends string>(name: TNext): TrackShapes<this, TNext> {
throw new Error('TODO Implement withShape.');
}
public withAnimation<TNext extends string>(name: TNext): TrackAnimations<this, TNext> {
throw new Error('TODO Implement withAnimation.');
}
public withOrder(shape: TShape, animation: TAnimation[]): this {
throw new Error('TODO Implement withOrder.');
}
}
Что там происходит?
Мы определяем два типа аккумуляторов для SlideBuilder
. Они получают существующие SlideBuilder
, infer
формы и типы анимации, используют объединение типов, чтобы расширить соответствующий тип generi c, а затем возвращают SlideBuilder
. Это самая продвинутая часть ответа.
Затем внутри start
мы используем never
, чтобы инициализировать SlideBuilder
нулями (так сказать). Это полезно, потому что объединение T | never
равно T
(аналогично тому, как 5 + 0 = 5
).
Теперь каждый вызов withShape
и withAnimation
использует соответствующий аккумулятор в качестве типа возврата. Это означает, что каждый вызов соответствующим образом расширяет тип и классифицирует аргумент в соответствующем сегменте!
Обратите внимание, что withShape
и withAnimation
generics extend string
. Это ограничивает тип до string
. Это также предотвращает расширение строкового литерала до string
. Это означает, что вызывающим абонентам не нужно использовать as const
и, следовательно, предоставляется более дружественный API.
Результат? Мы "отслеживаем" типы аргументов! Вот некоторые тесты, которые показывают, как он соответствует требованиям.
Тестовые случаи
// Passes type checking.
SlideBuilder
.start()
.withShape("Shape1")
.withAnimation('Animation1')
.withOrder("Shape1", ["Animation1"])
// Passes type checking.
SlideBuilder
.start()
.withShape("Shape1")
.withAnimation('Animation1')
.withAnimation('Animation2')
.withOrder("Shape1", ["Animation1", "Animation2"])
// Fails type checking.
SlideBuilder
.start()
.withShape("Shape1")
.withAnimation('Animation1')
.withAnimation('Animation2')
.withOrder("Foo", ["Animation1", "Animation2"])
// Fails type checking.
SlideBuilder
.start()
.withShape("Shape1")
.withAnimation('Animation1')
.withAnimation('Animation2')
.withOrder("Shape1", ["Foo", "Animation2"])
Эволюция ответа
Наконец, вот некоторые ссылки на игровые площадки, которые показывают эволюцию из этого ответа:
Playground Link Показывает исходное решение, которое поддерживает только фигуры и требует as const
.
Playground Link Приносит анимацию в класс и до сих пор использует as const
.
Playground Link Устраняет необходимость в as const
и обеспечивает почти готовое решение.