From 49140829fef1eff4c158e18fe9ba9088d8e28579 Mon Sep 17 00:00:00 2001 From: hyesungoh Date: Mon, 19 Oct 2026 16:13:46 +0900 Subject: [PATCH 1/3] refactor: keep `any` out of implementation code Raise no-explicit-any to an error outside tests and move the one allowed `any` into the internal CallerTypedArgs type used by the callback APIs. Reword the principle docs and agent instructions to match. Refs #568 --- .changeset/explicit-any-policy.md | 5 +++++ .cursorrules | 2 +- .github/copilot-instructions.md | 2 +- AGENTS.md | 2 +- CLAUDE.md | 2 +- docs/design-principles.md | 2 +- docs/es/design-principles.md | 2 +- docs/hook-design-principles.md | 13 +++++++++---- docs/ja/design-principles.md | 2 +- docs/ko/design-principles.md | 2 +- docs/ko/hook-design-principles.md | 15 ++++++++++----- docs/zh-Hans/design-principles.md | 2 +- eslint.config.js | 9 ++++++++- .../react-simplikit/references/useThrottle.md | 2 +- packages/react-simplikit/src/_internal/types.ts | 7 +++++++ .../useCallbackOncePerRender.ts | 7 +++++-- .../src/hooks/useDebounce/debounce.ts | 8 ++++---- .../src/hooks/useDebounce/useDebounce.ts | 4 ++-- .../usePreservedCallback/usePreservedCallback.ts | 6 ++++-- .../src/hooks/useStorageState/useStorageState.ts | 6 +++--- .../src/hooks/useThrottle/es/useThrottle.md | 2 +- .../src/hooks/useThrottle/ja/useThrottle.md | 2 +- .../src/hooks/useThrottle/ko/useThrottle.md | 2 +- .../src/hooks/useThrottle/throttle.ts | 6 +++--- .../src/hooks/useThrottle/useThrottle.md | 2 +- .../src/hooks/useThrottle/useThrottle.ts | 6 +++--- .../src/hooks/useThrottle/zh-Hans/useThrottle.md | 2 +- 27 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 .changeset/explicit-any-policy.md create mode 100644 packages/react-simplikit/src/_internal/types.ts diff --git a/.changeset/explicit-any-policy.md b/.changeset/explicit-any-policy.md new file mode 100644 index 00000000..cc6e7c30 --- /dev/null +++ b/.changeset/explicit-any-policy.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +The emitted declarations of `usePreservedCallback`, `useDebounce`, `useThrottle` and `useCallbackOncePerRender` now name their callback parameter type `CallerTypedArgs` instead of `any[]`. It is the same type, so no call site changes. `useThrottle` also constrains the callback's return type to `unknown` instead of `any`, which accepts the same callbacks. diff --git a/.cursorrules b/.cursorrules index ed285b6a..cea479d1 100644 --- a/.cursorrules +++ b/.cursorrules @@ -15,7 +15,7 @@ Unidirectional layers: `components → hooks → utils → _internal` - `type` over `interface` for type aliases - Named functions in useEffect: `useEffect(function handleResize() { ... }, [])` - Named exports only, no default exports -- No `any` — strict TypeScript +- No `any` in implementation (lint error) — for the parameters of a caller-supplied function, use `CallerTypedArgs` from `_internal/types.ts` - `.ts`/`.tsx` extensions in source imports (tsdown rewrites to `.mjs`/`.cjs`) - No implicit boolean coercion: `if (value)` → `if (value != null)` (enforced by `strict-boolean-expressions`) - Nullish checks: `== null` for both null and undefined, `!== undefined` only when distinction matters diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f8a6ddb3..625bceb0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,7 +12,7 @@ - Use `type` not `interface` for type aliases - Named functions in useEffect: `useEffect(function handleResize() { ... }, [])` - No default exports — named exports only -- No `any` types — strict TypeScript +- No `any` in implementation (lint error) — for the parameters of a caller-supplied function, use `CallerTypedArgs` from `_internal/types.ts` - Use `.ts`/`.tsx` extensions in source imports (tsdown rewrites them to `.mjs`/`.cjs`) - No implicit boolean coercion: `if (value)` → `if (value != null)` (enforced by `strict-boolean-expressions`) - Nullish checks: `== null` for both null and undefined, `!== undefined` only when distinction matters diff --git a/AGENTS.md b/AGENTS.md index 4e92374c..a4323009 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ src/hooks/useHookName/ - **No implicit boolean coercion** — `if (value)` → `if (value != null)` (enforced by `strict-boolean-expressions`) - **Import extensions** — Use `.ts`/`.tsx` extensions in source imports (tsdown rewrites them to `.mjs`/`.cjs` in the output) - **Named exports only** — No default exports -- **No `any` types** — Full TypeScript strict mode +- **No `any` in implementation** — Full TypeScript strict mode, and lint rejects `any`. For the parameter list of a function the caller passes in, use `CallerTypedArgs` from `src/_internal/types.ts` - **Zero runtime dependencies** ### Nullish Checks and Control Flow diff --git a/CLAUDE.md b/CLAUDE.md index 2809c868..6d150066 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ src/ - **useEffect cleanup** — Always return cleanup to remove listeners/subscriptions - **`"use client"` banner** — tsdown adds this to every emitted file for RSC compatibility - **Named exports only** — No default exports -- **No `any` types** — Full TypeScript strict mode, no escape hatches +- **No `any` in implementation** — Full TypeScript strict mode, and lint rejects `any`. The only exception is `CallerTypedArgs` in `src/_internal/types.ts`, the parameter list of a function the caller passes in - **Zero dependencies** — No runtime dependencies in `react-simplikit`. `react-simplikit-codemod` is a dev-time CLI and declares `commander`, `detect-indent`, `fast-glob`, `semver` and `typescript` in `dependencies` ### SSR-Safe Coding Pattern diff --git a/docs/design-principles.md b/docs/design-principles.md index bda94d86..47155a3e 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -38,4 +38,4 @@ The documentation is written in English and translated into several languages; a - **Strict Type Definitions**: All parameters, return values, and options are fully typed - **IntelliSense Support**: Get autocompletion and inline documentation in your IDE - **Generic Types**: Flexible APIs that preserve your type information -- **No `any` Types**: We avoid escape hatches that compromise type safety +- **No `any` in Implementation**: Implementation code never uses `any`. The one exception is APIs that take your function: their parameter types come from your own annotations, so unannotated parameters end up as `any` diff --git a/docs/es/design-principles.md b/docs/es/design-principles.md index b707e640..cc6d6e80 100644 --- a/docs/es/design-principles.md +++ b/docs/es/design-principles.md @@ -38,4 +38,4 @@ La documentación se escribe en inglés y se traduce a varios idiomas; una pági - **Definiciones de tipos estrictas**: todos los parámetros, valores de retorno y opciones están completamente tipados - **Compatibilidad con IntelliSense**: obtén autocompletado y documentación integrada en tu IDE - **Tipos genéricos**: APIs flexibles que preservan tu información de tipos -- **Sin tipos `any`**: evitamos las vías de escape que comprometen la seguridad de tipos +- **Sin `any` en la implementación**: el código de implementación nunca usa `any`. La única excepción son las APIs que reciben tu función: los tipos de sus parámetros vienen de tus propias anotaciones, así que los parámetros sin anotar quedan como `any` diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index 32589f44..84b7e5c2 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -115,16 +115,21 @@ useEffect(function tick() { }, []); ``` -#### C4. No `any` Types +#### C4. No `any` in Implementation -Use generics ``. `any` propagates and defeats the type system. Justified `eslint-disable` with comment is acceptable for generic callback types. +Use generics ``. `any` propagates and defeats the type system, so lint rejects it in implementation code. The only exception is the parameter list of a function the caller passes in: `unknown[]` would reject annotated callbacks, because function parameters are compared contravariantly. Use the shared `CallerTypedArgs` type from `_internal/types.ts` there instead of writing `any`. ```ts // ✅ Generic function useDebounce(value: T, delay: number): T; -// ✅ Justified exception (comment required) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback +// ✅ Caller's function: its parameter types come from the caller's annotations +function useThrottle unknown>( + callback: F, + wait: number +); + +// ❌ `any` written directly anywhere outside `_internal/types.ts`, even with an eslint-disable comment type AnyFunction = (...args: any[]) => unknown; ``` diff --git a/docs/ja/design-principles.md b/docs/ja/design-principles.md index 8e5886fa..e4a7a599 100644 --- a/docs/ja/design-principles.md +++ b/docs/ja/design-principles.md @@ -38,4 +38,4 @@ - **厳密な型定義**: すべてのパラメータ、戻り値、オプションが完全に型付けされています - **IntelliSense サポート**: IDE で自動補完とインラインドキュメントを利用できます - **ジェネリック型**: 型情報を保持する柔軟な API を提供します -- **`any` 型を使用しない**: 型安全性を損なうエスケープハッチを避けています +- **実装コードで `any` を使用しない**: 実装コードでは `any` を使いません。唯一の例外は関数を受け取る API です。そのパラメータの型は利用者自身の型注釈で決まるため、型注釈のないパラメータは `any` になります diff --git a/docs/ko/design-principles.md b/docs/ko/design-principles.md index b73a9e4e..110823f4 100644 --- a/docs/ko/design-principles.md +++ b/docs/ko/design-principles.md @@ -38,4 +38,4 @@ - **엄격한 타입 정의**: 모든 매개변수, 반환 값, 옵션이 완전히 타입화되어 있어요 - **IntelliSense 지원**: IDE에서 자동완성과 인라인 문서를 제공받을 수 있어요 - **제네릭 타입**: 타입 정보를 보존하는 유연한 API를 제공해요 -- **`any` 타입 없음**: 타입 안전성을 손상시키는 escape hatch를 사용하지 않아요 +- **구현 코드에 `any` 없음**: 구현 코드에서는 `any`를 쓰지 않아요. 유일한 예외는 함수를 받는 API예요. 이 API의 매개변수 타입은 사용하는 쪽에서 직접 적은 타입을 따르기 때문에, 타입을 적지 않은 매개변수는 `any`가 돼요 diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md index 89aee0af..75659e8b 100644 --- a/docs/ko/hook-design-principles.md +++ b/docs/ko/hook-design-principles.md @@ -107,16 +107,21 @@ useEffect(function tick() { }, []); ``` -#### C4. No `any` Types +#### C4. No `any` in Implementation -제네릭 `` 사용. any 전파 → 타입 시스템 무력화. 정당한 사유(generic callback 등) 시 per-line eslint-disable + 코멘트 허용. +제네릭 `` 사용. `any`는 전파되어 타입 시스템 무력화 → 구현 코드의 `any`는 lint가 거부. 유일한 예외는 호출자가 넘기는 함수의 파라미터 목록. 함수 파라미터는 반공변적으로 비교 → `unknown[]`은 타입을 명시한 콜백을 거부. 이 자리에는 `any` 대신 `_internal/types.ts`의 공용 타입 `CallerTypedArgs` 사용. ```ts -// ✅ Generic +// ✅ 제네릭 function useDebounce(value: T, delay: number): T; -// ✅ 정당한 예외 (코멘트 필수) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback +// ✅ 호출자의 함수: 파라미터 타입은 호출자가 명시한 타입을 따름 +function useThrottle unknown>( + callback: F, + wait: number +); + +// ❌ `_internal/types.ts` 밖에서 `any`를 직접 씀 (eslint-disable 주석이 있어도 금지) type AnyFunction = (...args: any[]) => unknown; ``` diff --git a/docs/zh-Hans/design-principles.md b/docs/zh-Hans/design-principles.md index 14fabd47..ae6635dc 100644 --- a/docs/zh-Hans/design-principles.md +++ b/docs/zh-Hans/design-principles.md @@ -38,4 +38,4 @@ - **严格的类型定义**:所有参数、返回值和选项都有完整的类型 - **IntelliSense 支持**:在 IDE 中获得自动补全和内联文档 - **泛型**:灵活的 API,保留你的类型信息 -- **不使用 `any` 类型**:我们避免使用会破坏类型安全的脱围机制 +- **实现代码不使用 `any`**:实现代码从不使用 `any`。唯一的例外是接收你的函数的 API:其参数类型由你自己的类型注解决定,因此未标注类型的参数会变成 `any` diff --git a/eslint.config.js b/eslint.config.js index c5602687..ce3f46f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -75,7 +75,7 @@ export default defineConfig( import: importPlugin, }, rules: { - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/strict-boolean-expressions': 'error', '@typescript-eslint/consistent-type-definitions': ['warn', 'type'], 'react/react-in-jsx-scope': 'off', @@ -124,5 +124,12 @@ export default defineConfig( }, }, }, + { + // Only implementation code must be free of `any`; tests may use it for mocks and fixtures. + files: ['**/*.{spec,test}.{ts,tsx}', '**/_internal/test-utils/**'], + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, prettierRecommended ); diff --git a/packages/plugin/skills/react-simplikit/references/useThrottle.md b/packages/plugin/skills/react-simplikit/references/useThrottle.md index a4068503..1995c67d 100644 --- a/packages/plugin/skills/react-simplikit/references/useThrottle.md +++ b/packages/plugin/skills/react-simplikit/references/useThrottle.md @@ -7,7 +7,7 @@ such as when handling scroll or resize events. ## Interface ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } diff --git a/packages/react-simplikit/src/_internal/types.ts b/packages/react-simplikit/src/_internal/types.ts new file mode 100644 index 00000000..b4c38765 --- /dev/null +++ b/packages/react-simplikit/src/_internal/types.ts @@ -0,0 +1,7 @@ +/** + * Parameter list of a function the caller passes in. + * The caller's own annotations decide the types, and unannotated parameters end up as `any`. + * `unknown[]` would reject annotated callbacks, because function parameters are compared contravariantly. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the only `any` allowed in implementation code +export type CallerTypedArgs = any[]; diff --git a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts index ded97fc9..5e3215a1 100644 --- a/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts +++ b/packages/react-simplikit/src/hooks/useCallbackOncePerRender/useCallbackOncePerRender.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { DependencyList, useEffect, useRef } from 'react'; +import type { CallerTypedArgs } from '../../_internal/types.ts'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; /** @@ -39,7 +39,10 @@ import { usePreservedCallback } from '../usePreservedCallback/index.ts'; * return
User page
; * } */ -export function useCallbackOncePerRender void>(callback: F, deps: DependencyList) { +export function useCallbackOncePerRender void>( + callback: F, + deps: DependencyList +) { // Same reason as `useAsyncEffect`: the body itself is compiler-clean, but React Compiler // bails on any function carrying a React ESLint suppression, and the // `react-hooks/exhaustive-deps` suppression below is unavoidable for a caller-supplied `deps`. diff --git a/packages/react-simplikit/src/hooks/useDebounce/debounce.ts b/packages/react-simplikit/src/hooks/useDebounce/debounce.ts index a956065b..f332eb29 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/debounce.ts +++ b/packages/react-simplikit/src/hooks/useDebounce/debounce.ts @@ -1,7 +1,8 @@ // Simplified version of https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic function constraint requires `any[]` due to parameter contravariance -export type DebouncedFunction void> = { +import type { CallerTypedArgs } from '../../_internal/types.ts'; + +export type DebouncedFunction void> = { (...args: Parameters): void; cancel: () => void; }; @@ -17,8 +18,7 @@ type DebounceOptions = { edges?: Array<'leading' | 'trailing'>; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic function constraint requires `any[]` due to parameter contravariance -export function debounce void>( +export function debounce void>( func: F, debounceMs: number, { edges = ['leading', 'trailing'] }: DebounceOptions = {} diff --git a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts index 7e535687..06cb8a95 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts +++ b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts @@ -1,7 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect } from 'react'; import { useMemo } from 'react'; +import type { CallerTypedArgs } from '../../_internal/types.ts'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; import { debounce, DebouncedFunction } from './debounce.ts'; @@ -61,7 +61,7 @@ type DebounceOptions = { * ); * } */ -export function useDebounce unknown>( +export function useDebounce unknown>( callback: F, wait: number, options: DebounceOptions = {} diff --git a/packages/react-simplikit/src/hooks/usePreservedCallback/usePreservedCallback.ts b/packages/react-simplikit/src/hooks/usePreservedCallback/usePreservedCallback.ts index 1b92232c..884b35f5 100644 --- a/packages/react-simplikit/src/hooks/usePreservedCallback/usePreservedCallback.ts +++ b/packages/react-simplikit/src/hooks/usePreservedCallback/usePreservedCallback.ts @@ -1,5 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useEffect, useRef } from 'react'; + +import type { CallerTypedArgs } from '../../_internal/types.ts'; + /** * @description * `usePreservedCallback` is a React hook that maintains a stable reference to a callback function @@ -29,7 +31,7 @@ import { useCallback, useEffect, useRef } from 'react'; * return ; * } */ -export function usePreservedCallback( +export function usePreservedCallback( callback: (...args: Arguments) => ReturnValue ) { const callbackRef = useRef(callback); diff --git a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts index b63349d9..cb4be452 100644 --- a/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts +++ b/packages/react-simplikit/src/hooks/useStorageState/useStorageState.ts @@ -21,7 +21,7 @@ type StorageStateOptionsWithSerializer = StorageStateOptions & { deserializer: (value: string) => Serializable; }; -type SerializableGuard = T[0] extends any +type SerializableGuard = T[0] extends unknown ? T : T[0] extends never ? 'Received a non-serializable value' @@ -33,7 +33,7 @@ const emitListeners = () => { listeners.forEach(listener => listener()); }; -function isPlainObject(value: unknown): value is Record { +function isPlainObject(value: unknown): value is Record { if (typeof value !== 'object') { return false; } @@ -49,7 +49,7 @@ function isPlainObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === '[object Object]'; } -const ensureSerializable = (value: T): SerializableGuard => { +const ensureSerializable = (value: T): SerializableGuard => { if ( value[0] != null && !['string', 'number', 'boolean'].includes(typeof value[0]) && diff --git a/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md index 951cf042..caab8812 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/es/useThrottle.md @@ -7,7 +7,7 @@ por ejemplo, al manejar eventos de desplazamiento o de cambio de tamaño. ## Interfaz ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } diff --git a/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md index f1598fb5..b4f0fe33 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/ja/useThrottle.md @@ -6,7 +6,7 @@ ## インターフェース ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } diff --git a/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md index 09cabf2d..0e85f5ac 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/ko/useThrottle.md @@ -5,7 +5,7 @@ ## 인터페이스 ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } diff --git a/packages/react-simplikit/src/hooks/useThrottle/throttle.ts b/packages/react-simplikit/src/hooks/useThrottle/throttle.ts index 40315b16..bdceb302 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/throttle.ts +++ b/packages/react-simplikit/src/hooks/useThrottle/throttle.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ // Simplified version of https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts +import type { CallerTypedArgs } from '../../_internal/types.ts'; import { debounce } from '../useDebounce/debounce.ts'; type ThrottleOptions = { @@ -14,12 +14,12 @@ type ThrottleOptions = { edges?: Array<'leading' | 'trailing'>; }; -type ThrottledFunction void> = { +type ThrottledFunction void> = { (...args: Parameters): void; cancel: () => void; }; -export function throttle void>( +export function throttle void>( func: F, throttleMs: number, { edges = ['leading', 'trailing'] }: ThrottleOptions = {} diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md index a4068503..1995c67d 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.md @@ -7,7 +7,7 @@ such as when handling scroll or resize events. ## Interface ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts index 6d600ecb..8a87a571 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect, useMemo } from 'react'; +import type { CallerTypedArgs } from '../../_internal/types.ts'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; import { usePreservedReference } from '../usePreservedReference/index.ts'; @@ -12,7 +12,7 @@ import { throttle } from './throttle.ts'; * This is useful for limiting the rate at which a function can be called, * such as when handling scroll or resize events. * - * @template {(...args: any[]) => any} F - The type of the callback function. + * @template {(...args: any[]) => unknown} F - The type of the callback function. * @param {F} callback - The function to be throttled. * @param {number} wait - The number of milliseconds to throttle invocations to. * @param {{ edges?: Array<'leading' | 'trailing'> }} [options] - Options to control the behavior of the throttle. @@ -32,7 +32,7 @@ import { throttle } from './throttle.ts'; * }; * }, [throttledScroll]); */ -export function useThrottle any>( +export function useThrottle unknown>( callback: F, wait: number, options?: Parameters[2] diff --git a/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md b/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md index 3689d4c2..bb17ea98 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md +++ b/packages/react-simplikit/src/hooks/useThrottle/zh-Hans/useThrottle.md @@ -5,7 +5,7 @@ ## 接口 ```ts -function useThrottle any>( +function useThrottle unknown>( callback: F, wait: number, options?: { edges?: Array<'leading' | 'trailing'> } From 96f1bb5b2ee70456d6141853b3f3802b26dba4d2 Mon Sep 17 00:00:00 2001 From: hyesungoh Date: Mon, 19 Oct 2026 23:45:36 +0900 Subject: [PATCH 2/3] docs: scope the `any` exception to APIs that wrap a function Callback parameters take their types from annotations or the inference context, not from annotations alone, and fixed-type callbacks such as useInputState's never fall back to `any`. --- docs/design-principles.md | 2 +- docs/es/design-principles.md | 2 +- docs/ja/design-principles.md | 2 +- docs/ko/design-principles.md | 2 +- docs/zh-Hans/design-principles.md | 2 +- packages/react-simplikit/src/_internal/types.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design-principles.md b/docs/design-principles.md index 47155a3e..1308fd93 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -38,4 +38,4 @@ The documentation is written in English and translated into several languages; a - **Strict Type Definitions**: All parameters, return values, and options are fully typed - **IntelliSense Support**: Get autocompletion and inline documentation in your IDE - **Generic Types**: Flexible APIs that preserve your type information -- **No `any` in Implementation**: Implementation code never uses `any`. The one exception is APIs that take your function: their parameter types come from your own annotations, so unannotated parameters end up as `any` +- **No `any` in Implementation**: Implementation code never uses `any`. The one exception is generic APIs that wrap your function, such as `usePreservedCallback`: the parameter types of the function you pass come from your annotations or the inference context, and fall back to `any` when there is neither diff --git a/docs/es/design-principles.md b/docs/es/design-principles.md index cc6d6e80..765a1ec9 100644 --- a/docs/es/design-principles.md +++ b/docs/es/design-principles.md @@ -38,4 +38,4 @@ La documentación se escribe en inglés y se traduce a varios idiomas; una pági - **Definiciones de tipos estrictas**: todos los parámetros, valores de retorno y opciones están completamente tipados - **Compatibilidad con IntelliSense**: obtén autocompletado y documentación integrada en tu IDE - **Tipos genéricos**: APIs flexibles que preservan tu información de tipos -- **Sin `any` en la implementación**: el código de implementación nunca usa `any`. La única excepción son las APIs que reciben tu función: los tipos de sus parámetros vienen de tus propias anotaciones, así que los parámetros sin anotar quedan como `any` +- **Sin `any` en la implementación**: el código de implementación nunca usa `any`. La única excepción son las APIs genéricas que envuelven tu función, como `usePreservedCallback`: los tipos de los parámetros de tu función vienen de tus anotaciones o del contexto de inferencia, y quedan como `any` cuando no hay ninguno de los dos diff --git a/docs/ja/design-principles.md b/docs/ja/design-principles.md index e4a7a599..d8744fe9 100644 --- a/docs/ja/design-principles.md +++ b/docs/ja/design-principles.md @@ -38,4 +38,4 @@ - **厳密な型定義**: すべてのパラメータ、戻り値、オプションが完全に型付けされています - **IntelliSense サポート**: IDE で自動補完とインラインドキュメントを利用できます - **ジェネリック型**: 型情報を保持する柔軟な API を提供します -- **実装コードで `any` を使用しない**: 実装コードでは `any` を使いません。唯一の例外は関数を受け取る API です。そのパラメータの型は利用者自身の型注釈で決まるため、型注釈のないパラメータは `any` になります +- **実装コードで `any` を使用しない**: 実装コードでは `any` を使いません。唯一の例外は、`usePreservedCallback` のように渡された関数をラップするジェネリック API です。渡した関数のパラメータの型は、利用者自身の型注釈または推論のコンテキストで決まり、どちらもない場合は `any` になります diff --git a/docs/ko/design-principles.md b/docs/ko/design-principles.md index 110823f4..34740126 100644 --- a/docs/ko/design-principles.md +++ b/docs/ko/design-principles.md @@ -38,4 +38,4 @@ - **엄격한 타입 정의**: 모든 매개변수, 반환 값, 옵션이 완전히 타입화되어 있어요 - **IntelliSense 지원**: IDE에서 자동완성과 인라인 문서를 제공받을 수 있어요 - **제네릭 타입**: 타입 정보를 보존하는 유연한 API를 제공해요 -- **구현 코드에 `any` 없음**: 구현 코드에서는 `any`를 쓰지 않아요. 유일한 예외는 함수를 받는 API예요. 이 API의 매개변수 타입은 사용하는 쪽에서 직접 적은 타입을 따르기 때문에, 타입을 적지 않은 매개변수는 `any`가 돼요 +- **구현 코드에 `any` 없음**: 구현 코드에서는 `any`를 쓰지 않아요. 유일한 예외는 `usePreservedCallback`처럼 전달한 함수를 감싸는 제네릭 API예요. 전달한 함수의 매개변수 타입은 직접 명시한 타입이나 추론 맥락에서 정해지고, 둘 다 없으면 `any`가 돼요 diff --git a/docs/zh-Hans/design-principles.md b/docs/zh-Hans/design-principles.md index ae6635dc..8a347385 100644 --- a/docs/zh-Hans/design-principles.md +++ b/docs/zh-Hans/design-principles.md @@ -38,4 +38,4 @@ - **严格的类型定义**:所有参数、返回值和选项都有完整的类型 - **IntelliSense 支持**:在 IDE 中获得自动补全和内联文档 - **泛型**:灵活的 API,保留你的类型信息 -- **实现代码不使用 `any`**:实现代码从不使用 `any`。唯一的例外是接收你的函数的 API:其参数类型由你自己的类型注解决定,因此未标注类型的参数会变成 `any` +- **实现代码不使用 `any`**:实现代码从不使用 `any`。唯一的例外是像 `usePreservedCallback` 这样包装你的函数的泛型 API:传入函数的参数类型由你自己的类型注解或推断上下文决定,两者都没有时会变成 `any` diff --git a/packages/react-simplikit/src/_internal/types.ts b/packages/react-simplikit/src/_internal/types.ts index b4c38765..33b06759 100644 --- a/packages/react-simplikit/src/_internal/types.ts +++ b/packages/react-simplikit/src/_internal/types.ts @@ -1,6 +1,6 @@ /** * Parameter list of a function the caller passes in. - * The caller's own annotations decide the types, and unannotated parameters end up as `any`. + * The types come from the caller's annotations or the inference context, and fall back to `any` when there is neither. * `unknown[]` would reject annotated callbacks, because function parameters are compared contravariantly. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the only `any` allowed in implementation code From c525f744febefc0159e8a2ac0c24162f241b2741 Mon Sep 17 00:00:00 2001 From: hyesungoh Date: Wed, 21 Oct 2026 12:59:17 +0900 Subject: [PATCH 3/3] fix: write out the return types of useThrottle and useDebounce A consumer that emits declarations got TS2742 when it exported their result: useThrottle's inferred type named CallerTypedArgs and useDebounce's named DebouncedFunction, and the package entry exports neither. The verify-pack consumer fixture now emits declarations and covers both. useThrottle's returned function now takes the callback's parameters instead of any[]. --- .changeset/explicit-any-policy.md | 6 ++++-- .../fixtures/consumer/tsconfig.bundler.json | 1 + .../fixtures/consumer/tsconfig.node16.json | 1 + .../verify-pack/fixtures/consumer/types-consumer.ts | 13 +++++++++++-- .../src/hooks/useDebounce/useDebounce.ts | 5 +++-- .../src/hooks/useThrottle/useThrottle.ts | 3 ++- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.changeset/explicit-any-policy.md b/.changeset/explicit-any-policy.md index cc6e7c30..5a9d38db 100644 --- a/.changeset/explicit-any-policy.md +++ b/.changeset/explicit-any-policy.md @@ -1,5 +1,7 @@ --- -'react-simplikit': patch +'react-simplikit': minor --- -The emitted declarations of `usePreservedCallback`, `useDebounce`, `useThrottle` and `useCallbackOncePerRender` now name their callback parameter type `CallerTypedArgs` instead of `any[]`. It is the same type, so no call site changes. `useThrottle` also constrains the callback's return type to `unknown` instead of `any`, which accepts the same callbacks. +The emitted declarations of `usePreservedCallback`, `useDebounce`, `useThrottle` and `useCallbackOncePerRender` now name their callback parameter type `CallerTypedArgs` instead of `any[]`. It is the same type, so they accept the same callbacks. `useThrottle` also constrains the callback's return type to `unknown` instead of `any`, which accepts the same callbacks. + +The throttled function returned by `useThrottle` now accepts the callback's parameters instead of `any[]`, so calling it with arguments the callback does not take is a type error. The return types of `useThrottle` and `useDebounce` are now written out in their declarations, so a consumer that emits declarations can export their result without a TS2742 error. diff --git a/.scripts/verify-pack/fixtures/consumer/tsconfig.bundler.json b/.scripts/verify-pack/fixtures/consumer/tsconfig.bundler.json index 8cd6be74..ed8b62d8 100644 --- a/.scripts/verify-pack/fixtures/consumer/tsconfig.bundler.json +++ b/.scripts/verify-pack/fixtures/consumer/tsconfig.bundler.json @@ -4,6 +4,7 @@ "moduleResolution": "bundler", "target": "es2020", "strict": true, + "declaration": true, "noEmit": true, "skipLibCheck": false, "types": [] diff --git a/.scripts/verify-pack/fixtures/consumer/tsconfig.node16.json b/.scripts/verify-pack/fixtures/consumer/tsconfig.node16.json index 8ff10213..9fef1ee4 100644 --- a/.scripts/verify-pack/fixtures/consumer/tsconfig.node16.json +++ b/.scripts/verify-pack/fixtures/consumer/tsconfig.node16.json @@ -4,6 +4,7 @@ "moduleResolution": "node16", "target": "es2020", "strict": true, + "declaration": true, "noEmit": true, "skipLibCheck": false, "types": [] diff --git a/.scripts/verify-pack/fixtures/consumer/types-consumer.ts b/.scripts/verify-pack/fixtures/consumer/types-consumer.ts index 8961b502..b991215e 100644 --- a/.scripts/verify-pack/fixtures/consumer/types-consumer.ts +++ b/.scripts/verify-pack/fixtures/consumer/types-consumer.ts @@ -1,9 +1,18 @@ -import { useNetworkStatus, useToggle } from 'react-simplikit'; +import { useDebounce, useNetworkStatus, useThrottle, useToggle } from 'react-simplikit'; type UseToggleReturn = ReturnType; const toggleReturn: [boolean, () => void] = undefined as unknown as UseToggleReturn; type NetworkStatusReturn = ReturnType; -export { toggleReturn }; +// Inferred return types fail declaration emit (TS2742) when they reference a type the package entry does not export +function useThrottledLog() { + return useThrottle((y: number) => console.log(y), 16); +} + +function useDebouncedLog() { + return useDebounce((query: string) => console.log(query), 300); +} + +export { toggleReturn, useDebouncedLog, useThrottledLog }; export type { NetworkStatusReturn }; diff --git a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts index 06cb8a95..bd910a61 100644 --- a/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts +++ b/packages/react-simplikit/src/hooks/useDebounce/useDebounce.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import type { CallerTypedArgs } from '../../_internal/types.ts'; import { usePreservedCallback } from '../usePreservedCallback/index.ts'; -import { debounce, DebouncedFunction } from './debounce.ts'; +import { debounce } from './debounce.ts'; type DebounceOptions = { leading?: boolean; @@ -65,7 +65,8 @@ export function useDebounce unknown>( callback: F, wait: number, options: DebounceOptions = {} -): DebouncedFunction { + // Written out because `DebouncedFunction` is not exported from the package entry, so consumers emitting declarations cannot name it (TS2742) +): { (...args: Parameters): void; cancel: () => void } { const preservedCallback = usePreservedCallback(callback) as F; const { leading = false, trailing = true } = options; diff --git a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts index 8a87a571..c195f5d7 100644 --- a/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts +++ b/packages/react-simplikit/src/hooks/useThrottle/useThrottle.ts @@ -36,7 +36,8 @@ export function useThrottle unknown>( callback: F, wait: number, options?: Parameters[2] -) { + // Written out because the inferred type names `CallerTypedArgs`, which consumers emitting declarations cannot import (TS2742) +): { (...args: Parameters): void; cancel: () => void } { const preservedCallback = usePreservedCallback(callback); const preservedOptions = usePreservedReference(options ?? {});