nextjs-demo
next.js demo using react 19 rc
git clone https://9o.is/git/nextjs-demo.git
commit 5f05b3fa96c0a473a637cf0f7c3d7e639a243795 parent 6290c5ed2af6db368eb05bc6418a8a2c6c21df45 Author: Jul <jul@9o.is> Date: Fri, 16 Aug 2024 10:25:22 +0800 working migration Diffstat:
| A | src/app/events/page.tsx | | | 13 | +++++++++++++ |
| M | src/app/globals.css | | | 16 | ---------------- |
| M | src/app/page.tsx | | | 101 | +++++++------------------------------------------------------------------------ |
| A | src/components/headless/Alert.tsx | | | 10 | ++++++++++ |
| A | src/components/headless/Button.tsx | | | 12 | ++++++++++++ |
| A | src/components/headless/Checkbox.tsx | | | 13 | +++++++++++++ |
| A | src/components/headless/ComboBox.tsx | | | 23 | +++++++++++++++++++++++ |
| A | src/components/headless/Form.tsx | | | 79 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/components/headless/Progress.tsx | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/components/headless/Select.tsx | | | 22 | ++++++++++++++++++++++ |
| A | src/components/headless/index.ts | | | 8 | ++++++++ |
| A | src/components/hooks/fetch.ts | | | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/components/hooks/index.ts | | | 3 | +++ |
| A | src/components/hooks/intl.ts | | | 5 | +++++ |
| A | src/components/page/EventsPage/EventsPage.tsx | | | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/components/page/EventsPage/index.ts | | | 2 | ++ |
| A | src/components/page/EventsPage/useFilteredSHEvents.ts | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | src/components/page/EventsPage/useSHEvents.ts | | | 34 | ++++++++++++++++++++++++++++++++++ |
| A | src/components/page/index.ts | | | 2 | ++ |
| A | src/lib/index.ts | | | 3 | +++ |
| A | src/lib/invariant.ts | | | 5 | +++++ |
| A | src/lib/objects.ts | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/lib/pipe.ts | | | 303 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
23 files changed, 828 insertions(+), 109 deletions(-)
diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx @@ -0,0 +1,12 @@ +import { EventsPage } from "@/components/page" +import { getEvents, SHNodeEvent } from "@/components/page/EventsPage/useSHEvents" + +async function getSHEvents() { + const res = await fetch('http://localhost:3000/node-events/node-a') + const node: SHNodeEvent = await res.json() + return getEvents(node) +} + +export default () => { + return <EventsPage loadedEvents={getSHEvents()} /> +} +\ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css @@ -2,20 +2,4 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; -} -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - color: var(--foreground); - background: var(--background); - font-synthesis: none; -} diff --git a/src/app/page.tsx b/src/app/page.tsx @@ -1,100 +1,15 @@ -import Image from "next/image"; +async function getRandomNumber() { + const res = await fetch('https://www.randomnumberapi.com/api/v1.0/random') + const data: [number] = await res.json() + return data[0] +} + +export const revalidate = 0 export default function Home() { return ( <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"> - <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start"> - <Image - className="dark:invert" - src="/next.svg" - alt="Next.js logo" - width={180} - height={38} - priority - /> - <ol className="font-mono list-inside list-decimal text-sm text-center sm:text-left"> - <li className="mb-2"> - Get started by editing{" "} - <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold"> - src/app/page.tsx - </code> - </li> - <li>Save and see your changes instantly.</li> - </ol> - - <div className="flex gap-4 items-center flex-col sm:flex-row"> - <a - className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5" - href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - <Image - className="dark:invert" - src="/vercel.svg" - alt="Vercel logomark" - width={20} - height={20} - /> - Deploy now - </a> - <a - className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44" - href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - Read our docs - </a> - </div> - </main> - <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center"> - <a - className="flex items-center gap-2 hover:underline hover:underline-offset-4" - href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - <Image - aria-hidden - src="/file-text.svg" - alt="File icon" - width={16} - height={16} - /> - Learn - </a> - <a - className="flex items-center gap-2 hover:underline hover:underline-offset-4" - href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - <Image - aria-hidden - src="/window.svg" - alt="Window icon" - width={16} - height={16} - /> - Examples - </a> - <a - className="flex items-center gap-2 hover:underline hover:underline-offset-4" - href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - <Image - aria-hidden - src="/globe.svg" - alt="Globe icon" - width={16} - height={16} - /> - Go to nextjs.org → - </a> - </footer> + <p>{getRandomNumber()}</p> </div> ); } diff --git a/src/components/headless/Alert.tsx b/src/components/headless/Alert.tsx @@ -0,0 +1,9 @@ +import { memo } from "react" + +export const Alert = memo(({ error }: { error?: Error }) => { + return ( + <div role="alert"> + {error?.message ?? <> </>} + </div> + ) +}) +\ No newline at end of file diff --git a/src/components/headless/Button.tsx b/src/components/headless/Button.tsx @@ -0,0 +1,11 @@ +import { memo, ReactNode } from "react" + +type ButtonProps = { + children: ReactNode + type?: "submit" | "button" | "reset" + disabled?: boolean +} + +export const Button = memo(({ type = "button", ...props }: ButtonProps) => { + return <button type={type} {...props} /> +}) +\ No newline at end of file diff --git a/src/components/headless/Checkbox.tsx b/src/components/headless/Checkbox.tsx @@ -0,0 +1,12 @@ +import { useId, memo } from "react" + +export const Checkbox = memo(({ label, name }: { label: string, name: string }) => { + const id = useId() + + return ( + <> + <input id={id} name={name} type="checkbox" /> + <label htmlFor={id}>{label}</label> + </> + ) +}) +\ No newline at end of file diff --git a/src/components/headless/ComboBox.tsx b/src/components/headless/ComboBox.tsx @@ -0,0 +1,22 @@ +import { useId, memo } from "react" + +export type ComboBoxProps = { + label: string + name: string + options: string[] + } + +export const ComboBox = memo(({ label, name, options }: ComboBoxProps) => { + const inputId = useId() + const datalistId = useId() + + return ( + <> + <label htmlFor={inputId}>{label}</label> + <input id={inputId} name={name} type="text" list={datalistId} /> + <datalist id={datalistId}> + {options.map(option => <option key={option} value={option} />)} + </datalist> + </> + ) +}) +\ No newline at end of file diff --git a/src/components/headless/Form.tsx b/src/components/headless/Form.tsx @@ -0,0 +1,79 @@ +import { createContext, ReactNode, useContext, useRef, useState } from "react" + +type FormProps<T> = ({ + children: ReactNode + ['aria-label']?: string +}) & ({ + onSubmit?: (data: FormDataMap) => void + validator: undefined +} | { + onSubmit: (data: T) => void + validator: (data: FormDataMap) => T +}) + +export type FormDataMap = { + [key: string]: FormDataEntryValue | undefined +} + +type FormContextProps = { + formError?: string +} + +const FormContext = createContext<FormContextProps | undefined>(undefined) + +export function Form<T>({ onSubmit, validator, children, ...props }: FormProps<T>) { + const formRef = useRef<HTMLFormElement>(null) + const [formError, setFormError] = useState<string | undefined>() + + const internalOnSubmit = (event: React.FormEvent<HTMLFormElement>) => { + event.preventDefault() + + const formData = new FormData(formRef.current || undefined) + const data: FormDataMap = {} + + for (const [k, v] of formData.entries()) { + data[k] = v + } + + if (validator) { + try { + onSubmit(validator(data)) + } catch (error) { + setFormError((error as Error).message) + } + return + } + + onSubmit?.(data) + } + + return ( + <FormContext.Provider value={{ formError }}> + <form onSubmit={internalOnSubmit} ref={formRef} {...props}> + {children} + </form> + </FormContext.Provider> + ) +} + +type FormErrorProps = { + children: (error: string) => ReactNode +} + +function FormError({ children }: FormErrorProps) { + const { formError } = useFormContext() + + return ( + <div role="alert"> + {formError ? children(formError) : <> </>} + </div> + ) +} + +function useFormContext() { + const formContext = useContext(FormContext) + if (!formContext) throw new Error('useFormContext() must be used within a Form') + return formContext +} + +Form.Error = FormError diff --git a/src/components/headless/Progress.tsx b/src/components/headless/Progress.tsx @@ -0,0 +1,63 @@ +import { createContext, ReactNode, useContext, useId } from "react" + +type ProgressContext = { + labelId: string + progressBarId: string + loading: boolean +} + +type ProgressProps = { + loading: boolean + children: ReactNode +} + +const ProgressContext = createContext<ProgressContext | undefined>(undefined) + +export function useProgressContext() { + const context = useContext(ProgressContext) + if (!context) throw new Error('useProgressContext must be used within a Progress component') + return context +} + +export function Progress ({ loading, children }: ProgressProps) { + const labelId = useId() + const progressBarId = useId() + + return ( + <ProgressContext.Provider value={{ labelId, progressBarId, loading }}> + {children} + </ProgressContext.Provider> + ) +} + +function ProgressBar({ children }: { children: ReactNode }) { + const { progressBarId, labelId, loading } = useProgressContext() + + if (!loading) return <> </> + + return ( + <div id={progressBarId} role="progressbar" aria-labelledby={labelId}> + {children} + </div> + ) +} + +function ProgressLabel({ children }: { children: ReactNode }) { + const { labelId } = useProgressContext() + return ( + <span id={labelId}>{children}</span> + ) +} + +function ProgressContent({ children }: { children: ReactNode }) { + const { loading, progressBarId } = useProgressContext() + return ( + <div aria-live="polite" aria-describedby={loading ? progressBarId : undefined}> + {children} + </div> + ) +} + +Progress.Bar = ProgressBar +Progress.Label = ProgressLabel +Progress.Content = ProgressContent +\ No newline at end of file diff --git a/src/components/headless/Select.tsx b/src/components/headless/Select.tsx @@ -0,0 +1,21 @@ +import { useId, memo } from "react" + +type SelectProps = { + label: string + name: string + options: readonly string[] +} + +export const Select = memo(({ label, name, options }: SelectProps) => { + const id = useId() + + return ( + <> + <label htmlFor={id}>{label}</label> + <select id={id} name={name}> + <option value="">Select...</option> + {options.map(option => <option key={option}>{option}</option>)} + </select> + </> + ) +}) +\ No newline at end of file diff --git a/src/components/headless/index.ts b/src/components/headless/index.ts @@ -0,0 +1,7 @@ +export * from './Alert' +export * from './Button' +export * from './ComboBox' +export * from './Checkbox' +export * from './Form' +export * from './Progress' +export * from './Select' +\ No newline at end of file diff --git a/src/components/hooks/fetch.ts b/src/components/hooks/fetch.ts @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react" + +export function useFetch<T>(url: string) { + const [data, setData] = useState<T | undefined>() + const [error, setError] = useState<Error | undefined>() + const [loading, setLoading] = useState(false) + + useEffect(() => { + const controller = new AbortController() + const signal = controller.signal + + const fetchData = async () => { + setLoading(true) + + try { + const response = await fetch(url, { signal }) + + if (!response.ok) { + throw new Error("Bad request") + } + + setData(await response.json()) + setError(undefined) + } catch (error) { + setError(error as Error) + } finally { + setLoading(false) + } + } + + fetchData() + + return () => controller.abort() + }, [url, setData, setError, setLoading]) + + return { data, error, loading } +} + +export function useFetchPromise<T>(fetch: Promise<T>) { + const [data, setData] = useState<T | undefined>() + const [error, setError] = useState<Error | undefined>() + const [loading, setLoading] = useState(true) + + useEffect(() => { + const complete = async () => { + try { + setData(await fetch) + } catch (error) { + setError(error as Error) + } finally { + setLoading(false) + } + } + complete() + }, [fetch, setData, setError, setLoading]) + + return { data, error, loading } +} +\ No newline at end of file diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './fetch' +export * from './intl' +\ No newline at end of file diff --git a/src/components/hooks/intl.ts b/src/components/hooks/intl.ts @@ -0,0 +1,5 @@ + +export function useNumberFormatter(options?: Intl.NumberFormatOptions) { + const locale = global.window ? global.window.navigator.language : 'en-US' + return new Intl.NumberFormat(locale, options) +} diff --git a/src/components/page/EventsPage/EventsPage.tsx b/src/components/page/EventsPage/EventsPage.tsx @@ -0,0 +1,73 @@ +"use client" + +import { Checkbox, ComboBox, Select, Progress, Alert, Form, FormDataMap, Button } from '../../headless' +import { useFetchPromise, useNumberFormatter } from '../../hooks' +import { SHEvent } from './useSHEvents' +import { FilteredSHEventsInput, useFilteredSHEvents } from './useFilteredSHEvents' +import { invariant, isSortType, SORT_TYPES } from '../../../lib' +import { useMemo } from 'react' + +export function EventsPage({ loadedEvents }: { loadedEvents: Promise<SHEvent[]> }) { + const { data: unfilteredEvents = [], error, loading } = useFetchPromise(loadedEvents) + const { events, filterSHEvents } = useFilteredSHEvents(unfilteredEvents) + const priceFormatter = useNumberFormatter({ + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0 + }) + + const cities = useMemo(() => + [...new Set(unfilteredEvents.map(({ city }) => city))], [unfilteredEvents]) + + return ( + <> + <Alert error={error} /> + <Form aria-label='Filter Events' onSubmit={filterSHEvents} validator={validator}> + <ComboBox label="City" name="city" options={cities} /> + <Select label="Sort by price" name="priceSort" options={SORT_TYPES} /> + <Checkbox label="Find Cheapest" name="cheapest" /> + <Button type="submit" disabled={loading}>Search</Button> + <Form.Error>{(error) => error}</Form.Error> + </Form> + <Progress loading={loading}> + <Progress.Bar> + <Progress.Label>Loading events...</Progress.Label> + </Progress.Bar> + <Progress.Content> + <ul> + {events.map(({ id, city, price }) => ( + <li key={id}> + <div>City: {city}</div> + <div>Price: {priceFormatter.format(price)}</div> + </li> + ))} + </ul> + </Progress.Content> + </Progress> + </> + ) +} + + +function validator ({ city, priceSort, cheapest }: FormDataMap): FilteredSHEventsInput { + invariant( + typeof city === 'string' && city.length < 255, + "City is invalid" + ) + + invariant( + cheapest === undefined || cheapest === 'on', + "Find cheapest input is invalid" + ) + + invariant( + priceSort === '' || (typeof priceSort === 'string' && isSortType(priceSort)), + "Sort by price is invalid" + ) + + return { + city, + priceSort: priceSort || undefined, + cheapest: cheapest === 'on', + } +} diff --git a/src/components/page/EventsPage/index.ts b/src/components/page/EventsPage/index.ts @@ -0,0 +1 @@ +export * from "./EventsPage" +\ No newline at end of file diff --git a/src/components/page/EventsPage/useFilteredSHEvents.ts b/src/components/page/EventsPage/useFilteredSHEvents.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react" +import { asArray, filterByProp, minByProp, pipe, sortByProp, SortType } from "../../../lib" +import { SHEvent } from "./useSHEvents" + +export type FilteredSHEventsInput = { + city?: string + priceSort?: SortType + cheapest?: boolean +} + +const sortByPrice = (priceSort: FilteredSHEventsInput['priceSort']) => + (events: SHEvent[]) => priceSort ? sortByProp('price', priceSort, events) : events + +const filterByCity = (city: FilteredSHEventsInput['city']) => + (events: SHEvent[]) => city ? filterByProp("city", city, events) : events + +const filterCheapest = (cheapest: FilteredSHEventsInput['cheapest']) => + (events: SHEvent[]) => cheapest ? minByProp('price', events) : events + + +export function useFilteredSHEvents(initialEvents: SHEvent[]) { + const [events, setEvents] = useState<SHEvent[]>(initialEvents) + + useEffect(() => { + setEvents(initialEvents) + }, [initialEvents]) + + const filterSHEvents = ({ city, priceSort, cheapest }: FilteredSHEventsInput) => { + setEvents(pipe( + initialEvents, + filterByCity(city), + sortByPrice(priceSort), + filterCheapest(cheapest), + asArray + )) + } + + return { events, filterSHEvents } +} +\ No newline at end of file diff --git a/src/components/page/EventsPage/useSHEvents.ts b/src/components/page/EventsPage/useSHEvents.ts @@ -0,0 +1,34 @@ +import { useMemo } from "react" +// import { useFetch } from "../../hooks" + +export type SHEvent = { + id: string + city: string + price: number +} + +type SHNodeEventId = { + id: string +} + +export type SHNodeEvent = SHNodeEventId & ({ + events: SHEvent[] + children: [] +} | { + events: [] + children: SHNodeEvent[] +}) + +// export function useSHEvents(id: SHNodeEvent['id']) { +// const { data, ...rest } = useFetch<SHNodeEvent>(`http://localhost:3000/node-events/${id}`) + +// return { +// ...rest, +// events: useMemo(() => data ? getEvents(data) : [], [data]), +// } +// } + +export function getEvents({ events, children }: SHNodeEvent): SHEvent[] { + if (events.length > 0) return events + return children.flatMap(getEvents) +} diff --git a/src/components/page/index.ts b/src/components/page/index.ts @@ -0,0 +1 @@ +export * from "./EventsPage" +\ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts @@ -0,0 +1,3 @@ +export * from './invariant' +export * from './objects' +export * from './pipe' diff --git a/src/lib/invariant.ts b/src/lib/invariant.ts @@ -0,0 +1,5 @@ + +export function invariant(condition: any, message?: string): asserts condition { + if (condition) return + throw new Error(message || "Invariant failed") +} diff --git a/src/lib/objects.ts b/src/lib/objects.ts @@ -0,0 +1,47 @@ +type KeysOfType<T, U> = { + [K in keyof T]: T[K] extends U ? K : never +}[keyof T] + +type NumberKeysOf<T> = KeysOfType<T, number> + +export const SORT_TYPES = ["ascending", "descending"] as const +export type SortType = typeof SORT_TYPES[number] + +export function isSortType(value: string | undefined | null): value is SortType { + return value === "ascending" || value === "descending" +} + +export function asArray<T>(value: T | T[] | undefined | null): T[] { + return Array.isArray(value) ? value : + value !== undefined && value !== null ? [value] : [] +} + +export function filterByProp<T extends object>(prop: keyof T, value: T[keyof T], objects: T[]): T[] { + return objects.filter(object => object[prop] === value) +} + +export function sortByProp<T,>(prop: NumberKeysOf<T>, order: SortType, objects: T[]): T[] { + return [...objects].sort((a: T, b: T) => { + if (!order || typeof a[prop] !== "number" || typeof b[prop] !== "number") return 0 + if (order === "ascending") return a[prop] - b[prop] + return b[prop] - a[prop] + }) +} + +export function minByProp<T extends object>(prop: NumberKeysOf<T>, objects: T[]): T | null { + if (objects.length === 0) return null + if (objects.length === 1) return objects[0] + + const [fst, snd, ...tail] = objects + const min = fst[prop] < snd[prop] ? fst : snd + return minByProp(prop, [min, ...tail]) +} + +// Alternative implementation using reduce +// const minByProp = <T extends object>(prop: NumberKeysOf<T>, objects: T[]): T | undefined => { +// if (objects.length === 0) return undefined + +// return objects.reduce((minObj, currentObj) => { +// return currentObj[prop] < minObj[prop] ? currentObj : minObj +// }) +// } diff --git a/src/lib/pipe.ts b/src/lib/pipe.ts @@ -0,0 +1,302 @@ +/** + * Pipes the value of an expression into a pipeline of functions. + * + * This is useful in combination with data-last functions as a simulation of methods: + * + * ``` + * as.map(f).filter(g) -> pipe(as, map(f), filter(g)) + * ``` + * + * See also {@link flow}. + * + * @example + * import { pipe } from "@fp-ts/core/Function" + * + * const length = (s: string): number => s.length + * const double = (n: number): number => n * 2 + * const decrement = (n: number): number => n - 1 + * + * assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9) + * + * @since 1.0.0 + */ +export function pipe<A>(a: A): A +export function pipe<A, B>(a: A, ab: (a: A) => B): B +export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C +export function pipe<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D +export function pipe<A, B, C, D, E>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): E +export function pipe<A, B, C, D, E, F>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): F +export function pipe<A, B, C, D, E, F, G>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): G +export function pipe<A, B, C, D, E, F, G, H>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): H +export function pipe<A, B, C, D, E, F, G, H, I>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): I +export function pipe<A, B, C, D, E, F, G, H, I, J>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): J +export function pipe<A, B, C, D, E, F, G, H, I, J, K>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K +): K +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L +): L +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M +): M +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N +): N +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O +): O + +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P +): P + +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q +): Q + +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R +): R + +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S +): S + +export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T +): T +export function pipe( + a: unknown, + ab?: Function, + bc?: Function, + cd?: Function, + de?: Function, + ef?: Function, + fg?: Function, + gh?: Function, + hi?: Function +): unknown { + switch (arguments.length) { + case 1: + return a + case 2: + return ab!(a) + case 3: + return bc!(ab!(a)) + case 4: + return cd!(bc!(ab!(a))) + case 5: + return de!(cd!(bc!(ab!(a)))) + case 6: + return ef!(de!(cd!(bc!(ab!(a))))) + case 7: + return fg!(ef!(de!(cd!(bc!(ab!(a)))))) + case 8: + return gh!(fg!(ef!(de!(cd!(bc!(ab!(a))))))) + case 9: + return hi!(gh!(fg!(ef!(de!(cd!(bc!(ab!(a)))))))) + default: { + let ret = arguments[0] + for (let i = 1; i < arguments.length; i++) { + ret = arguments[i](ret) + } + return ret + } + } +} +\ No newline at end of file