Типизация TypeScript

Popmotion предоставляет функциональную модель анимаций, в которой ключевую роль играют значения, трансформации и композиция потоков данных. При использовании TypeScript основной задачей становится строгая типизация потоков значений, конфигураций анимаций и функций преобразования, что позволяет выявлять ошибки на этапе компиляции и стабилизировать поведение анимационных цепочек.


Базовые типы и модель значений

В основе типизации лежит описание того, какие значения могут участвовать в анимации. Popmotion оперирует универсальным понятием значения (value), которое может быть числом, строкой, объектом или сложной структурой.

Типизация таких значений обычно строится через обобщения:

type ValueType = number | string | { [key: string]: any }

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

interface Position {
  x: number
  y: number
}
const position: Position = {
  x: 0,
  y: 0
}

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


Типизация tween-анимаций

Tween является базовой единицей временной интерполяции. В TypeScript он обычно описывается через параметризированный интерфейс конфигурации:

interface TweenProps<T> {
  from: T
  to: T
  duration?: number
  ease?: (t: number) => number
  onUpdate?: (v: T) => void
}

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

Пример строго типизированного tween:

const fade: TweenProps<number> = {
  from: 0,
  to: 1,
  duration: 300,
  onUpdate: (v) => {
    const opacity: number = v
  }
}

Для сложных объектов используется глубокая типизация:

const move: TweenProps<Position> = {
  from: { x: 0, y: 0 },
  to: { x: 100, y: 200 }
}

Типизация физических анимаций (spring)

Spring-модель требует более сложной типизации, так как включает параметры физической симуляции и состояние системы.

interface SpringConfig<T> {
  from: T
  to: T
  stiffness: number
  damping: number
  mass: number
  velocity?: T
  onUpdate?: (v: T) => void
}

Особенность типизации spring заключается в том, что velocity должен быть совместим по структуре с типом T. Для примитивов это число, для объектов — структура, повторяющая форму состояния.

const spring: SpringConfig<number> = {
  from: 0,
  to: 1,
  stiffness: 120,
  damping: 20
}

Для векторных систем:

const springPosition: SpringConfig<Position> = {
  from: { x: 0, y: 0 },
  to: { x: 300, y: 400 },
  stiffness: 150,
  damping: 25,
  velocity: { x: 0, y: 0 }
}

Типизация функций интерполяции

Интерполяция является ключевым механизмом преобразования значений во времени. В TypeScript она выражается через универсальные функции:

type Interpolate<T> = (input: number) => T

Более точная модель учитывает диапазоны входных значений:

interface Interpolator<T> {
  (progress: number): T
  range?: [number, number]
}

Для числовых значений:

const interpolateNumber: Interpolator<number> = (t) => {
  return 0 + (100 - 0) * t
}

Для объектов:

const interpolatePosition: Interpolator<Position> = (t) => {
  return {
    x: 0 + (100 - 0) * t,
    y: 0 + (200 - 0) * t
  }
}

Типизация гарантирует, что форма возвращаемого значения сохраняется на протяжении всей анимации.


Типизация MotionValue и реактивных источников

В реактивной модели Popmotion используется концепция изменяемого значения, которое может быть подписано на обновления.

interface MotionValue<T> {
  get(): T
  set(v: T): void
  subscribe(fn: (v: T) => void): () => void
}

Дженерик T фиксирует тип состояния, что позволяет строить строго типизированные реактивные цепочки.

const x: MotionValue<number>
const position: MotionValue<Position>

При компоновке нескольких MotionValue важно сохранять соответствие типов в вычисляемых функциях:

const combined = (pos: MotionValue<Position>) => {
  pos.subscribe(({ x, y }) => {
    const sum: number = x + y
  })
}

Типизация transform-функций

Функции трансформации позволяют преобразовывать поток значений в новые значения. В TypeScript они описываются как чистые функции с фиксированным входом и выходом.

type Transform<I, O> = (input: I) => O

Простейший пример:

const double: Transform<number, number> = (v) => v * 2

Композиция трансформаций требует сохранения согласованности типов:

const toString: Transform<number, string> = (v) => `${v}`
const length: Transform<string, number> = (v) => v.length

Композиция:

const pipeline = (v: number): number => {
  return length(toString(double(v)))
}

TypeScript предотвращает ошибки при несоответствии промежуточных типов, что особенно важно в цепочках Popmotion-пайплайнов.


Типизация keyframes и сложных анимационных сценариев

Keyframes представляют собой последовательность состояний с временными метками.

interface Keyframe<T> {
  time: number
  value: T
}
interface KeyframeAnimation<T> {
  keyframes: Keyframe<T>[]
  duration: number
}

Пример строго типизированной анимации:

const opacityFrames: KeyframeAnimation<number> = {
  keyframes: [
    { time: 0, value: 0 },
    { time: 500, value: 1 },
    { time: 1000, value: 0 }
  ],
  duration: 1000
}

Для объектов:

const motionFrames: KeyframeAnimation<Position> = {
  keyframes: [
    { time: 0, value: { x: 0, y: 0 } },
    { time: 500, value: { x: 100, y: 50 } },
    { time: 1000, value: { x: 200, y: 0 } }
  ],
  duration: 1000
}

Обобщённые ограничения и защита типов

Сложные анимационные системы требуют ограничения дженериков для предотвращения некорректных типов.

type Numeric = number

interface NumericTween<T extends Numeric | Position> {
  from: T
  to: T
}

Для объектов часто применяется constraint на структуру:

type VectorLike = {
  x: number
  y: number
}
function animate<T extends VectorLike>(v: T) {
  return v.x + v.y
}

Типизация цепочек преобразований

Popmotion активно использует композицию функций, что требует строгой типизации цепочек.

type Pipe<I, O> = {
  (input: I): O
}

Расширенная композиция:

type Pipe2<A, B, C> = (input: A) => C

Пример цепочки:

const step1 = (v: number): number => v + 1
const step2 = (v: number): string => `${v}`
const step3 = (v: string): number => v.length

const pipeline = (v: number): number => step3(step2(step1(v)))

Каждый этап строго проверяется TypeScript, что делает невозможным несоответствие типов в цепочке трансформаций.


Интеграция типизации с анимационным движком

Внутренние структуры Popmotion могут быть типизированы через единый базовый контракт:

interface Animation<T> {
  start: () => void
  stop: () => void
  subscribe: (fn: (v: T) => void) => () => void
}

Это позволяет унифицировать различные типы анимаций под единый интерфейс, сохраняя при этом строгую типовую безопасность.

const animation: Animation<Position>
const fadeAnimation: Animation<number>

Типизация гарантирует, что подписчики получают корректные значения без необходимости проверки runtime-структур.


Расширение типов через пользовательские модели

В сложных системах типы Popmotion часто расширяются через доменные модели, отражающие бизнес-логику интерфейса.

interface UIState {
  opacity: number
  scale: number
  position: Position
}
const uiAnimation: Animation<UIState> = {
  start() {},
  stop() {},
  subscribe(fn) {
    fn({
      opacity: 1,
      scale: 1,
      position: { x: 0, y: 0 }
    })
    return () => {}
  }
}

Такой подход позволяет строить полностью типизированные анимационные слои, где каждая трансформация состояния проверяется компилятором, а структура данных остаётся согласованной на всём протяжении жизненного цикла анимации.