Skip to content

Commit

Permalink
feat: Restrict types for Spring and Tween
Browse files Browse the repository at this point in the history
Closes Improve Spring data typing, or improving it so it doesn't bother about unsupported data types sveltejs#14851
  • Loading branch information
webJose committed Dec 29, 2024
1 parent 32348a5 commit f0a6b6d
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 13 deletions.
16 changes: 14 additions & 2 deletions packages/svelte/src/motion/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ export interface Spring<T> extends Readable<T> {
stiffness: number;
}

/**
* Defines the primitive data types that Spring and Tween objects can calculate on.
*/
export type MotionPrimitive = number | Date;

/**
* Defines the type of objects Spring and Tween objects can work on.
*/
export interface MotionRecord {
[x: string]: MotionPrimitive | MotionRecord | (MotionPrimitive | MotionRecord)[];
}

/**
* A wrapper for a value that behaves in a spring-like fashion. Changes to `spring.target` will cause `spring.current` to
* move towards it over time, taking account of the `spring.stiffness` and `spring.damping` parameters.
Expand All @@ -36,7 +48,7 @@ export interface Spring<T> extends Readable<T> {
* ```
* @since 5.8.0
*/
export class Spring<T> {
export class Spring<T extends MotionRecord[string]> {
constructor(value: T, options?: SpringOpts);

/**
Expand All @@ -53,7 +65,7 @@ export class Spring<T> {
* </script>
* ```
*/
static of<U>(fn: () => U, options?: SpringOpts): Spring<U>;
static of<U extends MotionRecord[string]>(fn: () => U, options?: SpringOpts): Spring<U>;

/**
* Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it.
Expand Down
18 changes: 10 additions & 8 deletions packages/svelte/src/motion/spring.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @import { Task } from '#client' */
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
/** @import { Spring as SpringStore } from './public.js' */
/** @import { MotionRecord, Spring as SpringStore } from './public.js' */
import { writable } from '../store/shared/index.js';
import { loop } from '../internal/client/loop.js';
import { raf } from '../internal/client/timing.js';
Expand Down Expand Up @@ -58,7 +58,7 @@ function tick_spring(ctx, last_value, current_value, target_value) {
* The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience.
*
* @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead
* @template [T=any]
* @template {MotionRecord[string]} [T=any]
* @param {T} [value]
* @param {SpringOpts} [opts]
* @returns {SpringStore<T>}
Expand Down Expand Up @@ -159,17 +159,18 @@ export function spring(value, opts = {}) {
* <input type="range" bind:value={spring.target} />
* <input type="range" bind:value={spring.current} disabled />
* ```
* @template T
* @template {MotionRecord[string]} T
* @since 5.8.0
*/
export class Spring {
#stiffness = source(0.15);
#damping = source(0.8);
#precision = source(0.01);

#current = source(/** @type {T} */ (undefined));
#target = source(/** @type {T} */ (undefined));

#current;
#target;

// @ts-expect-error Undefined doesn't satisfy the constraint of T.
#last_value = /** @type {T} */ (undefined);
#last_time = 0;

Expand All @@ -187,7 +188,8 @@ export class Spring {
* @param {SpringOpts} [options]
*/
constructor(value, options = {}) {
this.#current.v = this.#target.v = value;
this.#current = source(value);
this.#target = source(value);

if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);
Expand All @@ -207,7 +209,7 @@ export class Spring {
* const spring = Spring.of(() => number);
* </script>
* ```
* @template U
* @template {MotionRecord[string]} U
* @param {() => U} fn
* @param {SpringOpts} [options]
*/
Expand Down
12 changes: 9 additions & 3 deletions packages/svelte/src/motion/tweened.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @import { Task } from '../internal/client/types' */
/** @import { Tweened } from './public' */
/** @import { MotionRecord, Tweened } from './public' */
/** @import { TweenedOptions } from './private' */
import { writable } from '../store/shared/index.js';
import { raf } from '../internal/client/timing.js';
Expand Down Expand Up @@ -170,11 +170,15 @@ export function tweened(value, defaults = {}) {
* <input type="range" bind:value={tween.target} />
* <input type="range" bind:value={tween.current} disabled />
* ```
* @template T
*
* Refer to the `MotionRecord` type to understand all possible value types that Tween can handle.
* @template {MotionRecord[string]} T
* @since 5.8.0
*/
export class Tween {
// @ts-expect-error Undefined doesn't satisfy the constraint of T.
#current = source(/** @type {T} */ (undefined));
// @ts-expect-error Undefined doesn't satisfy the constraint of T.
#target = source(/** @type {T} */ (undefined));

/** @type {TweenedOptions<T>} */
Expand Down Expand Up @@ -205,7 +209,9 @@ export class Tween {
* const tween = Tween.of(() => number);
* </script>
* ```
* @template U
*
* Refer to the `MotionRecord` type to understand all possible value types that Tween can handle.
* @template {MotionRecord[string]} U
* @param {() => U} fn
* @param {TweenedOptions<U>} [options]
*/
Expand Down

0 comments on commit f0a6b6d

Please sign in to comment.