From 70d5aee4bdf76ffe5dad2c0a186fb8604612e709 Mon Sep 17 00:00:00 2001 From: dvd233 <111864431+dvd233@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:50:33 +0800 Subject: [PATCH 1/2] fix: preserve non-nullable ref return types --- packages/hooks/src/useLatest/__tests__/index.spec.ts | 7 ++++++- packages/hooks/src/useLatest/index.ts | 3 ++- packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts | 4 +++- packages/hooks/src/useUnmountedRef/index.tsx | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/hooks/src/useLatest/__tests__/index.spec.ts b/packages/hooks/src/useLatest/__tests__/index.spec.ts index 3ac331a901..b600edbdef 100644 --- a/packages/hooks/src/useLatest/__tests__/index.spec.ts +++ b/packages/hooks/src/useLatest/__tests__/index.spec.ts @@ -1,10 +1,15 @@ import { renderHook } from '@testing-library/react'; -import { describe, expect, test } from 'vitest'; +import type { MutableRefObject } from 'react'; +import { describe, expect, expectTypeOf, test } from 'vitest'; import useLatest from '../index'; const setUp = (val: any) => renderHook((state) => useLatest(state), { initialProps: val }); describe('useLatest', () => { + test('should expose a non-nullable mutable ref type', () => { + expectTypeOf(useLatest).returns.toEqualTypeOf>(); + }); + test('useLatest with basic variable should work', async () => { const { result, rerender } = setUp(0); diff --git a/packages/hooks/src/useLatest/index.ts b/packages/hooks/src/useLatest/index.ts index d9897e2bcf..e6c49f52d4 100644 --- a/packages/hooks/src/useLatest/index.ts +++ b/packages/hooks/src/useLatest/index.ts @@ -1,6 +1,7 @@ +import type { MutableRefObject } from 'react'; import { useRef } from 'react'; -function useLatest(value: T) { +function useLatest(value: T): MutableRefObject { const ref = useRef(value); ref.current = value; diff --git a/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts b/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts index c86e7a0096..22c3ea8cd1 100644 --- a/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts +++ b/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts @@ -1,10 +1,12 @@ -import { describe, expect, test } from 'vitest'; +import type { MutableRefObject } from 'react'; +import { describe, expect, expectTypeOf, test } from 'vitest'; import { renderHook } from '../../utils/tests'; import useUnmountedRef from '../index'; describe('useUnmountedRef', () => { test('should work', async () => { const hook = renderHook(() => useUnmountedRef()); + expectTypeOf(hook.result.current).toEqualTypeOf>(); expect(hook.result.current.current).toBe(false); hook.rerender(); expect(hook.result.current.current).toBe(false); diff --git a/packages/hooks/src/useUnmountedRef/index.tsx b/packages/hooks/src/useUnmountedRef/index.tsx index 540786bf78..d03717a3ff 100644 --- a/packages/hooks/src/useUnmountedRef/index.tsx +++ b/packages/hooks/src/useUnmountedRef/index.tsx @@ -1,6 +1,7 @@ +import type { MutableRefObject } from 'react'; import { useEffect, useRef } from 'react'; -const useUnmountedRef = () => { +const useUnmountedRef = (): MutableRefObject => { const unmountedRef = useRef(false); useEffect(() => { unmountedRef.current = false; From 560bcb9813f2104384f31cc7c3d7b2f02f551ecb Mon Sep 17 00:00:00 2001 From: lxr <1076629390@qq.com> Date: Tue, 1 Sep 2026 08:02:02 +0800 Subject: [PATCH 2/2] test: verify mutable ref declarations --- packages/hooks/src/__tests__/refTypes.spec.ts | 56 +++++++++++++++++++ .../src/useLatest/__tests__/index.spec.ts | 7 +-- .../useUnmountedRef/__tests__/index.spec.ts | 4 +- 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 packages/hooks/src/__tests__/refTypes.spec.ts diff --git a/packages/hooks/src/__tests__/refTypes.spec.ts b/packages/hooks/src/__tests__/refTypes.spec.ts new file mode 100644 index 0000000000..752f63b715 --- /dev/null +++ b/packages/hooks/src/__tests__/refTypes.spec.ts @@ -0,0 +1,56 @@ +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; +import { describe, expect, test } from 'vitest'; + +const emitDeclaration = (sourceFile: string) => { + let declaration = ''; + const program = ts.createProgram([sourceFile], { + declaration: true, + emitDeclarationOnly: true, + jsx: ts.JsxEmit.React, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + strictNullChecks: true, + target: ts.ScriptTarget.ES2015, + types: [], + }); + + const diagnostics = ts.getPreEmitDiagnostics(program); + if (diagnostics.length > 0) { + throw new Error( + ts.formatDiagnosticsWithColorAndContext(diagnostics, { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => process.cwd(), + getNewLine: () => '\n', + }), + ); + } + + const emitResult = program.emit(undefined, (fileName, text) => { + if (fileName.endsWith('.d.ts')) { + declaration = text; + } + }); + + expect(emitResult.emitSkipped).toBe(false); + return declaration; +}; + +describe('ref return type declarations', () => { + test.each([ + [ + 'useLatest', + '../useLatest/index.ts', + 'declare function useLatest(value: T): MutableRefObject;', + ], + [ + 'useUnmountedRef', + '../useUnmountedRef/index.tsx', + 'declare const useUnmountedRef: () => MutableRefObject;', + ], + ])('%s should expose a non-nullable mutable ref', (_hook, sourcePath, expected) => { + const sourceFile = fileURLToPath(new URL(sourcePath, import.meta.url)); + expect(emitDeclaration(sourceFile)).toContain(expected); + }); +}); diff --git a/packages/hooks/src/useLatest/__tests__/index.spec.ts b/packages/hooks/src/useLatest/__tests__/index.spec.ts index b600edbdef..3ac331a901 100644 --- a/packages/hooks/src/useLatest/__tests__/index.spec.ts +++ b/packages/hooks/src/useLatest/__tests__/index.spec.ts @@ -1,15 +1,10 @@ import { renderHook } from '@testing-library/react'; -import type { MutableRefObject } from 'react'; -import { describe, expect, expectTypeOf, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import useLatest from '../index'; const setUp = (val: any) => renderHook((state) => useLatest(state), { initialProps: val }); describe('useLatest', () => { - test('should expose a non-nullable mutable ref type', () => { - expectTypeOf(useLatest).returns.toEqualTypeOf>(); - }); - test('useLatest with basic variable should work', async () => { const { result, rerender } = setUp(0); diff --git a/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts b/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts index 22c3ea8cd1..c86e7a0096 100644 --- a/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts +++ b/packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts @@ -1,12 +1,10 @@ -import type { MutableRefObject } from 'react'; -import { describe, expect, expectTypeOf, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { renderHook } from '../../utils/tests'; import useUnmountedRef from '../index'; describe('useUnmountedRef', () => { test('should work', async () => { const hook = renderHook(() => useUnmountedRef()); - expectTypeOf(hook.result.current).toEqualTypeOf>(); expect(hook.result.current.current).toBe(false); hook.rerender(); expect(hook.result.current.current).toBe(false);