Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Restrict types for Spring and Tween #14862

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading