Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/fix-pick-animated-from-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@react-spring/core': patch
---

fix(core): infer all animated keys when a partial `from` is provided

`PickAnimated` returned only the `from` shape whenever a `from` prop was present, dropping forward props, `to` keys, and the other transition phases. `useSpring({ width: 100, height: 100, from: { width: 0 } })` typed its result as `{ width }`, so `styles.height` was a compile error even though `height` animates at runtime. It now merges `from` with the `to`, forward, and transition-phase values.
9 changes: 7 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,15 @@ jobs:
- name: Build
run: pnpm build-ci

- name: Test
- name: Check repo types
run: |
pnpm tsc --version
pnpm test:ts
pnpm check:types

- name: Test types
run: pnpm test:types



test-e2e:
needs: [build]
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"build-ci": "turbo run build --filter=!@react-spring/docs",
"build": "turbo run build",
"changeset": "changeset",
"check:types": "tsc --noEmit",
"clean": "turbo run clean",
"dev": "turbo run dev --no-cache --parallel --continue",
"docs:dev": "pnpm --filter @react-spring/docs dev",
Expand All @@ -36,12 +37,12 @@
"lint": "oxlint packages/*/src targets/*/src docs/app docs/scripts demo/src",
"package": "turbo run pack",
"prepare": "husky",
"test": "pnpm test:ts && pnpm test:unit && pnpm test:e2e",
"test": "pnpm test:unit && pnpm test:types && pnpm test:e2e",
"test:unit": "vitest run --project unit",
"test:cov": "vitest run --project unit --coverage",
"test:ts": "tsc --noEmit",
"test:types": "vitest run --project types",
"test:e2e": "vitest run --project e2e",
"release": "pnpm clean && pnpm install && pnpm build && pnpm test:ts && pnpm test:unit && pnpm changeset publish --no-git-tag",
"release": "pnpm clean && pnpm install && pnpm build && pnpm changeset publish --no-git-tag",
"vers": "pnpm changeset version"
},
"commitlint": {
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/SpringValue.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { it, expectTypeOf } from 'vitest'

import { SpringValue } from './SpringValue'

/**
* Guard for #2183 — "Incorrect result type in SpringValue `onChange` handler".
*
* The reported symptom is a TYPE/RUNTIME mismatch: the type claims
* `result.value` is `number`, but at runtime it logs `undefined` mid-animation.
* This test pins the type-level contract the issue relies on (the param is a
* fully-typed `AnimationResult`, `result.value` is `number`, not `any`).
*
* Scenario lives inside a never-invoked function so the constructor is
* type-checked by tsc but never executed by Vitest's runtime collection pass.
*/
it('#2183: SpringValue onChange result is typed, result.value is number (not any)', () => {
function scenario() {
// eslint-disable-next-line no-new
new SpringValue(0, {
onChange(result) {
expectTypeOf(result).not.toBeAny()
expectTypeOf(result.value).toEqualTypeOf<number>()
},
})
}
expectTypeOf(scenario).toBeFunction()
})
46 changes: 46 additions & 0 deletions packages/core/src/components/Spring.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react'
import { it, expectTypeOf } from 'vitest'

import { SpringValue } from '../SpringValue'
import { Spring } from './Spring'

/**
* Guard for #2006 — "render prop within <Spring> is inferred to any".
*
* The render prop should be inferred from `from`/`to` as a `SpringValues`
* object (e.g. `{ opacity: SpringValue<number>, color: SpringValue<string> }`),
* not `any` (which breaks consumers under `noImplicitAny`).
*/
// `to`-based overload (state inferred from `to`).
it('#2006: <Spring to> render prop is inferred as SpringValues, not any', () => {
function scenario() {
return (
<Spring to={{ opacity: 1, color: 'blue' }}>
{styles => {
expectTypeOf(styles).not.toBeAny()
expectTypeOf(styles.opacity).toEqualTypeOf<SpringValue<number>>()
expectTypeOf(styles.color).toEqualTypeOf<SpringValue<string>>()
return null
}}
</Spring>
)
}
expectTypeOf(scenario).toBeFunction()
})

// `from`-based overload (overload 1) — the exact shape from the issue's repo.
it('#2006: <Spring from> render prop is inferred as SpringValues, not any', () => {
function scenario() {
return (
<Spring from={{ opacity: 0, color: 'red' }}>
{styles => {
expectTypeOf(styles).not.toBeAny()
expectTypeOf(styles.opacity).toEqualTypeOf<SpringValue<number>>()
expectTypeOf(styles.color).toEqualTypeOf<SpringValue<string>>()
return null
}}
</Spring>
)
}
expectTypeOf(scenario).toBeFunction()
})
37 changes: 37 additions & 0 deletions packages/core/src/hooks/useSpring.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { it, expectTypeOf } from 'vitest'

import { useSpring } from './useSpring'

/**
* Guard for a known `any` leak found while sweeping the public surface for
* #2541.
*
* The per-key (object) form of an event handler — `onChange: { x: result => … }`
* — should type `result.value` as the key's value type (here, `number`). It
* does not: `result.value` is `any` at the inline call site.
*
* Root cause is a TypeScript limitation, NOT a wrong type definition: a callback
* written inline in the same object literal that `useSpring`'s generic `Props`
* is inferred from cannot be contextually typed from that (still-inferring)
* generic, so the param degrades to `any`. With an explicit annotation
* (`const p: ControllerProps<{ x: number }> = …`) the same handler types
* `result.value` as `number`, which confirms the definitions are fine. A real
* fix needs the hooks to separate state-inference from handler-typing.
*
* The assertion below is the type we WANT. The `@ts-expect-error` suppresses the
* current mismatch; when the limitation is resolved the error disappears, the
* directive becomes unused (a type error in its own right), and this test goes
* red — at which point delete the directive.
*/
it('per-key onChange: result.value should match the key value type', () => {
function scenario() {
useSpring({
x: 0,
onChange: {
// @ts-expect-error known limitation: result.value is `any`, not `number` (#2541)
x: result => expectTypeOf(result.value).toEqualTypeOf<number>(),
},
})
}
expectTypeOf(scenario).toBeFunction()
})
55 changes: 55 additions & 0 deletions packages/core/src/hooks/useTransition.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { it, expectTypeOf } from 'vitest'

import { SpringValue } from '../SpringValue'
import { useTransition } from './useTransition'

/**
* Guard for #1114 — "Type definition for `onDestroyed` is wrong".
*
* Originally typed `(isDestroyed: boolean) => void`; the issue asked for the
* transitioning item. The current source types it `(item: Item, key: Key)`,
* so this guard LOCKS IN that fix (item inferred from the data) rather than
* reproducing a regression.
*/
it('#1114: useTransition onDestroyed receives the typed item (not boolean/any)', () => {
function scenario() {
const items: number[] = [1, 2, 3]
useTransition(items, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
onDestroyed(item) {
expectTypeOf(item).not.toBeAny()
expectTypeOf(item).toEqualTypeOf<number>()
},
})
}
expectTypeOf(scenario).toBeFunction()
})

/**
* Guard for #1483 — "Type inference fails when useTransition styles are set via
* functions".
*
* When `from`/`enter`/`leave` are functions returning style objects, the
* animated state should still be inferred (e.g. `{ sx, sy }`), so the render
* prop's `styles` is `SpringValues<{ sx: number; sy: number }>` — not `{}`
* (the reported regression) nor `any`.
*/
it('#1483: useTransition infers animated state from function-style props', () => {
function scenario() {
const items: number[] = [1, 2, 3]
const transition = useTransition(items, {
from: () => ({ sx: 0, sy: 0 }),
enter: () => ({ sx: 1, sy: 1 }),
leave: () => ({ sx: 0, sy: 0 }),
})
transition(styles => {
expectTypeOf(styles).not.toBeAny()
expectTypeOf(styles.sx).toEqualTypeOf<SpringValue<number>>()
expectTypeOf(styles.sy).toEqualTypeOf<SpringValue<number>>()
return null
})
}
expectTypeOf(scenario).toBeFunction()
})
50 changes: 50 additions & 0 deletions packages/core/src/interpolate.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { it, expectTypeOf } from 'vitest'
import { assert, _ } from 'spec.ts'

import { interpolate } from './interpolate'
import { SpringValue } from './SpringValue'
import { Interpolation } from './Interpolation'

// Revived from the previously-orphaned spec.ts suite in
// `packages/core/src/types/__tests__/interpolate.ts`. Now co-located and run by
// the `types` project. Expectations updated: `interpolate` returns an
// `Interpolation`, not a `SpringValue`.
//
// `interpolate` is called inside never-invoked functions so the calls are
// type-checked by tsc but never executed by Vitest's runtime collection pass.

/** Return the arguments as-is */
const args = <T extends ReadonlyArray<any>>(...args: T) => args

it('with one SpringValue', () => {
function scenario() {
// Basic value (a single spring value spreads to a readonly array)
const out1 = interpolate(_ as SpringValue<number>, args)
assert(out1, _ as Interpolation<readonly number[]>)

// Array value
const out2 = interpolate(_ as SpringValue<[number, number]>, args)
assert(out2, _ as Interpolation<[number, number]>)
}
expectTypeOf(scenario).toBeFunction()
})

it('with an array of SpringValues', () => {
function scenario() {
// 1 value
const out1 = interpolate(_ as [SpringValue<number>], args)
assert(out1, _ as Interpolation<[number]>)

// 2 values
const out2 = interpolate(
_ as [SpringValue<number>, SpringValue<string>],
args
)
assert(out2, _ as Interpolation<[number, string]>)

// Infinite values
const out3 = interpolate(_ as SpringValue<number>[], args)
assert(out3, _ as Interpolation<number[]>)
}
expectTypeOf(scenario).toBeFunction()
})
114 changes: 0 additions & 114 deletions packages/core/src/types/__tests__/common.ts

This file was deleted.

Loading
Loading