diff --git a/.changeset/giant-moles-double.md b/.changeset/giant-moles-double.md new file mode 100644 index 00000000..dc3fa98b --- /dev/null +++ b/.changeset/giant-moles-double.md @@ -0,0 +1,7 @@ +--- +"www": minor +"eslint-config-custom": minor +"usehooks-ts": major +--- + +Prefer type over interface (#515) diff --git a/apps/www/src/app/(docs)/layout.tsx b/apps/www/src/app/(docs)/layout.tsx index effaa2f7..3504fabc 100644 --- a/apps/www/src/app/(docs)/layout.tsx +++ b/apps/www/src/app/(docs)/layout.tsx @@ -9,7 +9,7 @@ import { DocsSidebarNav } from '@/components/sidebar-nav' import { docsConfig } from '@/config/docs' import { siteConfig } from '@/config/site' -interface DocsLayoutProps { +type DocsLayoutProps = { children: React.ReactNode } diff --git a/apps/www/src/app/(marketing)/layout.tsx b/apps/www/src/app/(marketing)/layout.tsx index 1d63dafd..a84bef70 100644 --- a/apps/www/src/app/(marketing)/layout.tsx +++ b/apps/www/src/app/(marketing)/layout.tsx @@ -5,7 +5,7 @@ import { MainNav } from '@/components/main-nav' import { marketingConfig } from '@/config/marketing' import { siteConfig } from '@/config/site' -interface MarketingLayoutProps { +type MarketingLayoutProps = { children: React.ReactNode } diff --git a/apps/www/src/app/layout.tsx b/apps/www/src/app/layout.tsx index f0333d3c..4c413e66 100644 --- a/apps/www/src/app/layout.tsx +++ b/apps/www/src/app/layout.tsx @@ -17,7 +17,7 @@ const fontHeading = localFont({ variable: '--font-heading', }) -interface RootLayoutProps { +type RootLayoutProps = { children: React.ReactNode } diff --git a/apps/www/src/components/command-copy.tsx b/apps/www/src/components/command-copy.tsx index d24cee90..4bb735fc 100644 --- a/apps/www/src/components/command-copy.tsx +++ b/apps/www/src/components/command-copy.tsx @@ -15,10 +15,10 @@ import { } from './ui/dropdown-menu' import { cn } from '@/lib/utils' -interface CommandCopyProps extends ComponentProps<'code'> { +type CommandCopyProps = { command: Record | string defaultCommand?: string -} +} & ComponentProps<'code'> export function CommandCopy({ className, diff --git a/apps/www/src/components/docs-page-header.tsx b/apps/www/src/components/docs-page-header.tsx index 87a835fd..bad8dbe5 100644 --- a/apps/www/src/components/docs-page-header.tsx +++ b/apps/www/src/components/docs-page-header.tsx @@ -1,9 +1,9 @@ import { cn } from '@/lib/utils' -interface DocsPageHeaderProps extends React.HTMLAttributes { +type DocsPageHeaderProps = { heading: string text?: string -} +} & React.HTMLAttributes export function DocsPageHeader({ heading, diff --git a/apps/www/src/components/main-nav.tsx b/apps/www/src/components/main-nav.tsx index fee15883..6bc137fe 100644 --- a/apps/www/src/components/main-nav.tsx +++ b/apps/www/src/components/main-nav.tsx @@ -11,7 +11,7 @@ import { siteConfig } from '@/config/site' import { cn } from '@/lib/utils' import type { MainNavItem } from '@/types' -interface MainNavProps { +type MainNavProps = { items?: MainNavItem[] children?: React.ReactNode } diff --git a/apps/www/src/components/mobile-nav.tsx b/apps/www/src/components/mobile-nav.tsx index c7330d51..ad6f4be6 100644 --- a/apps/www/src/components/mobile-nav.tsx +++ b/apps/www/src/components/mobile-nav.tsx @@ -8,7 +8,7 @@ import { siteConfig } from '@/config/site' import { cn } from '@/lib/utils' import type { MainNavItem } from '@/types' -interface MobileNavProps { +type MobileNavProps = { items: MainNavItem[] children?: React.ReactNode } diff --git a/apps/www/src/components/paper.tsx b/apps/www/src/components/paper.tsx index a0612c8c..9b728989 100644 --- a/apps/www/src/components/paper.tsx +++ b/apps/www/src/components/paper.tsx @@ -5,7 +5,7 @@ import { buttonVariants } from '@/components/ui/button' import { getPosts } from '@/lib/mdx' import { cn } from '@/lib/utils' -interface DocsPagerProps { +type DocsPagerProps = { slug: string } diff --git a/apps/www/src/components/sidebar-nav.tsx b/apps/www/src/components/sidebar-nav.tsx index 34fa6919..2f71e177 100644 --- a/apps/www/src/components/sidebar-nav.tsx +++ b/apps/www/src/components/sidebar-nav.tsx @@ -6,7 +6,7 @@ import { usePathname } from 'next/navigation' import { cn } from '@/lib/utils' import type { SidebarNavItem } from '@/types' -export interface DocsSidebarNavProps { +type DocsSidebarNavProps = { items: SidebarNavItem[] } @@ -29,7 +29,7 @@ export function DocsSidebarNav({ items }: DocsSidebarNavProps) { ) : null } -interface DocsSidebarNavItemsProps { +type DocsSidebarNavItemsProps = { items: SidebarNavItem[] pathname: string | null } diff --git a/apps/www/src/components/table-of-content.tsx b/apps/www/src/components/table-of-content.tsx index 72b27757..f98c0dc7 100644 --- a/apps/www/src/components/table-of-content.tsx +++ b/apps/www/src/components/table-of-content.tsx @@ -94,7 +94,7 @@ function useActiveItem(itemIds: (string | undefined)[]) { return activeId } -interface TreeProps { +type TreeProps = { tree: TableOfContents level?: number activeItem?: string | null diff --git a/apps/www/src/components/ui/button.tsx b/apps/www/src/components/ui/button.tsx index a4d2d67c..f993132b 100644 --- a/apps/www/src/components/ui/button.tsx +++ b/apps/www/src/components/ui/button.tsx @@ -35,11 +35,10 @@ const buttonVariants = cva( }, ) -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { +export type ButtonProps = { asChild?: boolean -} +} & React.ButtonHTMLAttributes & + VariantProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { diff --git a/apps/www/src/types/index.d.ts b/apps/www/src/types/index.d.ts index f3cbae31..bf6110d8 100644 --- a/apps/www/src/types/index.d.ts +++ b/apps/www/src/types/index.d.ts @@ -44,7 +44,7 @@ export type MarketingConfig = { mainNav: MainNavItem[] } -export interface Post { +export type Post = { name: string // useHook slug: string // use-hook href: string // /react-hook/use-hook diff --git a/packages/eslint-config-custom/.eslintrc.cjs b/packages/eslint-config-custom/.eslintrc.cjs index 6b2dc1df..1458e2f8 100644 --- a/packages/eslint-config-custom/.eslintrc.cjs +++ b/packages/eslint-config-custom/.eslintrc.cjs @@ -84,9 +84,21 @@ module.exports = { }, ], + // Opinionated: no enums + 'no-restricted-syntax': [ + 'warn', + { + selector: 'TSEnumDeclaration', + message: 'Prefer union type', + }, + ], + + // Opinionated: prefer "type" over "interface" + '@typescript-eslint/consistent-type-definitions': ['warn', 'type'], + // Disable some TypeScript rules '@typescript-eslint/explicit-module-boundary-types': 'off', // Too noisy - '@typescript-eslint/consistent-type-definitions': 'off', // Will come in v3 + // '@typescript-eslint/consistent-type-definitions': 'off', // Will come in v3 '@typescript-eslint/no-unnecessary-condition': 'off', // TODO: Enable it '@typescript-eslint/prefer-ts-expect-error': 'off', }, diff --git a/packages/usehooks-ts/src/useBoolean/useBoolean.ts b/packages/usehooks-ts/src/useBoolean/useBoolean.ts index 4e485ac3..09dc3420 100644 --- a/packages/usehooks-ts/src/useBoolean/useBoolean.ts +++ b/packages/usehooks-ts/src/useBoolean/useBoolean.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import type { Dispatch, SetStateAction } from 'react' -interface UseBooleanOutput { +type UseBooleanOutput = { value: boolean setValue: Dispatch> setTrue: () => void diff --git a/packages/usehooks-ts/src/useCountdown/useCountdown.ts b/packages/usehooks-ts/src/useCountdown/useCountdown.ts index ed501830..e911b67d 100644 --- a/packages/usehooks-ts/src/useCountdown/useCountdown.ts +++ b/packages/usehooks-ts/src/useCountdown/useCountdown.ts @@ -4,14 +4,14 @@ import { useBoolean } from '../useBoolean' import { useCounter } from '../useCounter' import { useInterval } from '../useInterval' -interface CountdownOptions { +type CountdownOptions = { countStart: number intervalMs?: number isIncrement?: boolean countStop?: number } -interface CountdownControllers { +type CountdownControllers = { startCountdown: () => void stopCountdown: () => void resetCountdown: () => void diff --git a/packages/usehooks-ts/src/useCounter/useCounter.ts b/packages/usehooks-ts/src/useCounter/useCounter.ts index 72c5170f..e3bd54a6 100644 --- a/packages/usehooks-ts/src/useCounter/useCounter.ts +++ b/packages/usehooks-ts/src/useCounter/useCounter.ts @@ -2,7 +2,7 @@ import { useState } from 'react' import type { Dispatch, SetStateAction } from 'react' -interface UseCounterOutput { +type UseCounterOutput = { count: number increment: () => void decrement: () => void diff --git a/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts b/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts index 5ff6482c..cd61c276 100644 --- a/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts +++ b/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts @@ -11,7 +11,7 @@ type DarkModeOptions = { initializeWithValue?: boolean } -interface DarkModeOutput { +type DarkModeOutput = { isDarkMode: boolean toggle: () => void enable: () => void diff --git a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts index 2ccf8f04..59f3d4fa 100644 --- a/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts +++ b/packages/usehooks-ts/src/useDebounceCallback/useDebounceCallback.ts @@ -7,7 +7,7 @@ import { useUnmount } from '../useUnmount' /** * Configuration options for controlling the behavior of the debounced function. */ -export interface DebounceOptions { +export type DebounceOptions = { /** * Determines whether the function should be invoked on the leading edge of the timeout. */ @@ -25,7 +25,7 @@ export interface DebounceOptions { /** * Functions to manage a debounced callback. */ -interface ControlFunctions { +type ControlFunctions = { /** * Cancels pending function invocations. */ @@ -48,10 +48,10 @@ interface ControlFunctions { * Ensure proper handling in your code. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface DebouncedState ReturnType> - extends ControlFunctions { - (...args: Parameters): ReturnType | undefined -} +export type DebouncedState ReturnType> = (( + ...args: Parameters +) => ReturnType | undefined) & + ControlFunctions /** * Hook to create a debounced version of a callback function. diff --git a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts index 136d00ca..e3ce68b5 100644 --- a/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts +++ b/packages/usehooks-ts/src/useEventListener/useEventListener.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ import { fireEvent, renderHook } from '@testing-library/react' import { useEventListener } from './useEventListener' diff --git a/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.ts b/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.ts index fb391346..2b05c080 100644 --- a/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.ts +++ b/packages/usehooks-ts/src/useIntersectionObserver/useIntersectionObserver.ts @@ -20,11 +20,11 @@ type ObserverCallback = ( * @property {ObserverCallback} [onChange] - A callback function to be invoked when the intersection state changes. * @property {boolean} [initialIsIntersecting=false] - The initial state of the intersection. */ -interface IntersectionObserverOptions extends IntersectionObserverInit { +type IntersectionObserverOptions = { freezeOnceVisible?: boolean onChange?: ObserverCallback initialIsIntersecting?: boolean -} +} & IntersectionObserverInit /** Supports both array and object destructing */ type IntersectionResult = [ diff --git a/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts b/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts index f8d2daed..93ca3b5a 100644 --- a/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts +++ b/packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts @@ -6,12 +6,13 @@ import { useEventCallback } from '../useEventCallback' import { useEventListener } from '../useEventListener' declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface WindowEventMap { 'local-storage': CustomEvent } } -interface UseLocalStorageOptions { +type UseLocalStorageOptions = { serializer?: (value: T) => string deserializer?: (value: string) => T initializeWithValue?: boolean @@ -140,7 +141,7 @@ export function useLocalStorage( const handleStorageChange = useCallback( (event: StorageEvent | CustomEvent) => { - if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { + if ((event as StorageEvent).key && (event as StorageEvent).key !== key) { return } setStoredValue(readValue()) diff --git a/packages/usehooks-ts/src/useMap/useMap.ts b/packages/usehooks-ts/src/useMap/useMap.ts index a207bb9b..687382a0 100644 --- a/packages/usehooks-ts/src/useMap/useMap.ts +++ b/packages/usehooks-ts/src/useMap/useMap.ts @@ -12,7 +12,7 @@ export type MapOrEntries = Map | [K, V][] * @template K - The type of keys in the map. * @template V - The type of values in the map. */ -export interface Actions { +export type Actions = { set: (key: K, value: V) => void setAll: (entries: MapOrEntries) => void remove: (key: K) => void diff --git a/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.ts b/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.ts index 078701ff..bc276d12 100644 --- a/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.ts +++ b/packages/usehooks-ts/src/useReadLocalStorage/useReadLocalStorage.ts @@ -8,7 +8,7 @@ const IS_SERVER = typeof window === 'undefined' * Represents the type for the options available when reading from local storage. * @template T - The type of the stored value. */ -interface Options { +type Options = { deserializer?: (value: string) => T initializeWithValue: InitializeWithValue } @@ -103,7 +103,7 @@ export function useReadLocalStorage( const handleStorageChange = useCallback( (event: StorageEvent | CustomEvent) => { - if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { + if ((event as StorageEvent).key && (event as StorageEvent).key !== key) { return } setStoredValue(readValue()) diff --git a/packages/usehooks-ts/src/useResizeObserver/useResizeObserver.ts b/packages/usehooks-ts/src/useResizeObserver/useResizeObserver.ts index ed0cf288..e80ebf7c 100644 --- a/packages/usehooks-ts/src/useResizeObserver/useResizeObserver.ts +++ b/packages/usehooks-ts/src/useResizeObserver/useResizeObserver.ts @@ -59,10 +59,10 @@ export function useResizeObserver( const isMounted = useIsMounted() const previousSize = useRef({ ...initialSize }) const onResize = useRef(undefined) - onResize.current = options?.onResize + onResize.current = options.onResize useEffect(() => { - if (!ref?.current) return + if (!ref.current) return if (typeof window === 'undefined' || !('ResizeObserver' in window)) return @@ -86,7 +86,7 @@ export function useResizeObserver( previousSize.current.width = newWidth previousSize.current.height = newHeight - if (onResize?.current) { + if (onResize.current) { onResize.current(newSize) } else { if (isMounted()) { diff --git a/packages/usehooks-ts/src/useScreen/useScreen.ts b/packages/usehooks-ts/src/useScreen/useScreen.ts index 91603817..ec2f2a26 100644 --- a/packages/usehooks-ts/src/useScreen/useScreen.ts +++ b/packages/usehooks-ts/src/useScreen/useScreen.ts @@ -50,13 +50,13 @@ export function useScreen( const debouncedSetScreen = useDebounceCallback( setScreen, - options?.debounceDelay, + options.debounceDelay, ) /** Handles the resize event of the window. */ function handleSize() { const newScreen = readScreen() - const setSize = options?.debounceDelay ? debouncedSetScreen : setScreen + const setSize = options.debounceDelay ? debouncedSetScreen : setScreen if (newScreen) { // Create a shallow clone to trigger a re-render (#280). diff --git a/packages/usehooks-ts/src/useScript/useScript.ts b/packages/usehooks-ts/src/useScript/useScript.ts index 3a47f032..9c19366d 100644 --- a/packages/usehooks-ts/src/useScript/useScript.ts +++ b/packages/usehooks-ts/src/useScript/useScript.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error' -export interface UseScriptOptions { +export type UseScriptOptions = { shouldPreventLoad?: boolean removeOnUnmount?: boolean } diff --git a/packages/usehooks-ts/src/useScrollLock/useScrollLock.ts b/packages/usehooks-ts/src/useScrollLock/useScrollLock.ts index 7cc40b6f..38dffe69 100644 --- a/packages/usehooks-ts/src/useScrollLock/useScrollLock.ts +++ b/packages/usehooks-ts/src/useScrollLock/useScrollLock.ts @@ -2,13 +2,13 @@ import { useRef, useState } from 'react' import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' -interface UseScrollLockOptions { +type UseScrollLockOptions = { autoLock: boolean lockTarget: HTMLElement | string widthReflow: boolean } -interface UseScrollLockResult { +type UseScrollLockResult = { isLocked: boolean lock: () => void unlock: () => void diff --git a/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.ts b/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.ts index 14fd2fe8..30af764d 100644 --- a/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.ts +++ b/packages/usehooks-ts/src/useSessionStorage/useSessionStorage.ts @@ -6,6 +6,7 @@ import { useEventCallback } from '../useEventCallback' import { useEventListener } from '../useEventListener' declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface WindowEventMap { 'session-storage': CustomEvent } @@ -18,7 +19,7 @@ declare global { * @property {(value: T) => string} [serializer] - A function to serialize the value before storing it. * @property {(value: string) => T} [deserializer] - A function to deserialize the stored value. */ -interface UseSessionStorageOptions { +type UseSessionStorageOptions = { serializer?: (value: T) => string deserializer?: (value: string) => T initializeWithValue?: boolean @@ -149,7 +150,7 @@ export function useSessionStorage( const handleStorageChange = useCallback( (event: StorageEvent | CustomEvent) => { - if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { + if ((event as StorageEvent).key && (event as StorageEvent).key !== key) { return } setStoredValue(readValue()) diff --git a/packages/usehooks-ts/src/useStep/useStep.ts b/packages/usehooks-ts/src/useStep/useStep.ts index f2bf70be..0a566740 100644 --- a/packages/usehooks-ts/src/useStep/useStep.ts +++ b/packages/usehooks-ts/src/useStep/useStep.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import type { Dispatch, SetStateAction } from 'react' -interface Helpers { +type Helpers = { goToNextStep: () => void goToPrevStep: () => void reset: () => void diff --git a/packages/usehooks-ts/src/useWindowSize/useWindowSize.ts b/packages/usehooks-ts/src/useWindowSize/useWindowSize.ts index 8caff58a..9e6bf0d4 100644 --- a/packages/usehooks-ts/src/useWindowSize/useWindowSize.ts +++ b/packages/usehooks-ts/src/useWindowSize/useWindowSize.ts @@ -4,7 +4,7 @@ import { useDebounceCallback } from '../useDebounceCallback' import { useEventListener } from '../useEventListener' import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' -interface WindowSize { +type WindowSize = { width: T height: T } @@ -59,11 +59,11 @@ export function useWindowSize( const debouncedSetWindowSize = useDebounceCallback( setWindowSize, - options?.debounceDelay, + options.debounceDelay, ) function handleSize() { - const setSize = options?.debounceDelay + const setSize = options.debounceDelay ? debouncedSetWindowSize : setWindowSize