From 5cc2c13988b0915ace7968d186508dcb88287f99 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 26 Oct 2025 21:04:20 -0700 Subject: [PATCH 1/6] add `$withType()` to restrict isomorphicFn --- .../src/createIsomorphicFn.ts | 31 ++++++++++++++ .../src/tests/createIsomorphicFn.test-d.ts | 42 +++++++++++++++++++ .../client/createIsomorphicFnDestructured.tsx | 3 +- .../server/createIsomorphicFnDestructured.tsx | 3 +- .../createIsomorphicFnDestructured.tsx | 5 +++ 5 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-client-core/src/createIsomorphicFn.ts index 09a1be9ec13..ac94ad19201 100644 --- a/packages/start-client-core/src/createIsomorphicFn.ts +++ b/packages/start-client-core/src/createIsomorphicFn.ts @@ -21,7 +21,36 @@ export interface ClientOnlyFn, TClient> ) => IsomorphicFn } +type AnyFn = (...args: Array) => any +export interface ServerOnlyFnWithType, TReturnType> + extends IsomorphicFn { + client: ( + clientImpl: (...args: TArgs) => TReturnType, + ) => IsomorphicFn +} + +export interface ClientOnlyFnWithType, TReturnType> + extends IsomorphicFn { + server: ( + serverImpl: (...args: TArgs) => TReturnType, + ) => IsomorphicFn +} + +export interface IsomorphicFnWithType, TReturnType> + extends IsomorphicFn { + server: ( + serverImpl: (...args: TArgs) => TReturnType, + ) => ServerOnlyFnWithType + client: ( + clientImpl: (...args: TArgs) => TReturnType, + ) => ClientOnlyFnWithType +} + export interface IsomorphicFnBase extends IsomorphicFn { + $withType: () => IsomorphicFnWithType< + Parameters, + ReturnType + > server: , TServer>( serverImpl: (...args: TArgs) => TServer, ) => ServerOnlyFn @@ -35,7 +64,9 @@ export interface IsomorphicFnBase extends IsomorphicFn { // therefore we must return a dummy function that allows calling `server` and `client` method chains. export function createIsomorphicFn(): IsomorphicFnBase { return { + $withType: () => {}, server: () => ({ client: () => () => {} }), client: () => ({ server: () => () => {} }), } as any } + diff --git a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts index 89f427d8c64..7bf1d8168db 100644 --- a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts +++ b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts @@ -70,3 +70,45 @@ test('createIsomorphicFn with arguments', () => { expectTypeOf(fn2).toBeCallableWith(1, 'a') expectTypeOf(fn2).returns.toEqualTypeOf() }) + +test('createIsomorphicFn with type', () => { + const fn1 = createIsomorphicFn() + .$withType<(a: string) => string>() + .server((a) => { + expectTypeOf(a).toEqualTypeOf() + return 'data' + }) + .client((a) => { + expectTypeOf(a).toEqualTypeOf() + return 'data' + }) + expectTypeOf(fn1).toBeCallableWith('foo') + expectTypeOf(fn1).returns.toEqualTypeOf() + + const fn2 = createIsomorphicFn() + .$withType<(a: string, b: number) => boolean>() + .client((a, b) => { + expectTypeOf(a).toEqualTypeOf() + expectTypeOf(b).toEqualTypeOf() + return true as const + }) + .server((a, b) => { + expectTypeOf(a).toEqualTypeOf() + expectTypeOf(b).toEqualTypeOf() + return false as const + }) + expectTypeOf(fn2).toBeCallableWith('foo', 1) + expectTypeOf(fn2).returns.toEqualTypeOf() + + const _invalidFnReturn = createIsomorphicFn() + .$withType<(a: string) => string>() + .server(() => '1') + // @ts-expect-error - invalid return type + .client(() => 2) + + const _invalidFnArgs = createIsomorphicFn() + .$withType<(a: string) => string>() + .server((a: string) => 'data') + // @ts-expect-error - invalid argument type + .client((a: number) => 'data') +}) diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx index 837195fb9c4..de9494e8b09 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnDestructured.tsx @@ -13,4 +13,5 @@ function abstractedClientFn() { } const clientOnlyFnAbstracted = abstractedClientFn; const serverThenClientFnAbstracted = abstractedClientFn; -const clientThenServerFnAbstracted = abstractedClientFn; \ No newline at end of file +const clientThenServerFnAbstracted = abstractedClientFn; +const withTypeRestriction = () => 'client'; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx index 1656889e535..2d683c57633 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnDestructured.tsx @@ -13,4 +13,5 @@ function abstractedClientFn() { } const clientOnlyFnAbstracted = () => {}; const serverThenClientFnAbstracted = abstractedServerFn; -const clientThenServerFnAbstracted = abstractedServerFn; \ No newline at end of file +const clientThenServerFnAbstracted = abstractedServerFn; +const withTypeRestriction = () => 'server'; \ No newline at end of file diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx index e3102bcb715..45805800101 100644 --- a/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx +++ b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnDestructured.tsx @@ -33,3 +33,8 @@ const serverThenClientFnAbstracted = createIsomorphicFn() const clientThenServerFnAbstracted = createIsomorphicFn() .client(abstractedClientFn) .server(abstractedServerFn) + +const withTypeRestriction = createIsomorphicFn() + .$withType<() => string>() + .server(() => 'server') + .client(() => 'client') From 4d75ba66e00c2da1ac938b7a22617d5e543424e5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 26 Oct 2025 21:17:12 -0700 Subject: [PATCH 2/6] fix: force both implementations --- .../src/createIsomorphicFn.ts | 27 +++++++++++-------- .../src/tests/createIsomorphicFn.test-d.ts | 11 ++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-client-core/src/createIsomorphicFn.ts index ac94ad19201..3685b6805d9 100644 --- a/packages/start-client-core/src/createIsomorphicFn.ts +++ b/packages/start-client-core/src/createIsomorphicFn.ts @@ -36,14 +36,17 @@ export interface ClientOnlyFnWithType, TReturnType> ) => IsomorphicFn } -export interface IsomorphicFnWithType, TReturnType> - extends IsomorphicFn { - server: ( - serverImpl: (...args: TArgs) => TReturnType, - ) => ServerOnlyFnWithType - client: ( - clientImpl: (...args: TArgs) => TReturnType, - ) => ClientOnlyFnWithType +export interface IsomorphicFnWithType, TReturnType> { + server: (serverImpl: (...args: TArgs) => TReturnType) => { + client: ( + clientImpl: (...args: TArgs) => TReturnType, + ) => IsomorphicFn + } + client: (clientImpl: (...args: TArgs) => TReturnType) => { + server: ( + serverImpl: (...args: TArgs) => TReturnType, + ) => IsomorphicFn + } } export interface IsomorphicFnBase extends IsomorphicFn { @@ -63,10 +66,12 @@ export interface IsomorphicFnBase extends IsomorphicFn { // if we use `createIsomorphicFn` in this library itself, vite tries to execute it before the transformer runs // therefore we must return a dummy function that allows calling `server` and `client` method chains. export function createIsomorphicFn(): IsomorphicFnBase { - return { - $withType: () => {}, + const baseFns = { server: () => ({ client: () => () => {} }), client: () => ({ server: () => () => {} }), + } + return { + $withType: () => baseFns, + ...baseFns, } as any } - diff --git a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts index 7bf1d8168db..e82db9b8e37 100644 --- a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts +++ b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts @@ -100,6 +100,17 @@ test('createIsomorphicFn with type', () => { expectTypeOf(fn2).toBeCallableWith('foo', 1) expectTypeOf(fn2).returns.toEqualTypeOf() + const noServerImpl = createIsomorphicFn() + .$withType<(a: string) => string>() + .client((a) => 'data') + expectTypeOf(noServerImpl).not.toBeFunction() + + // Missing server implementation + const noClientImpl = createIsomorphicFn() + .$withType<(a: string) => string>() + .server((a) => 'data') + expectTypeOf(noClientImpl).not.toBeFunction() + const _invalidFnReturn = createIsomorphicFn() .$withType<(a: string) => string>() .server(() => '1') From ea98dbba8247621cc92b30b75955ed034e5365ac Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 04:18:18 +0000 Subject: [PATCH 3/6] ci: apply automated fixes --- .../src/tests/createIsomorphicFn.test-d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts index e82db9b8e37..b99061a8d45 100644 --- a/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts +++ b/packages/start-client-core/src/tests/createIsomorphicFn.test-d.ts @@ -103,12 +103,12 @@ test('createIsomorphicFn with type', () => { const noServerImpl = createIsomorphicFn() .$withType<(a: string) => string>() .client((a) => 'data') - expectTypeOf(noServerImpl).not.toBeFunction() + expectTypeOf(noServerImpl).not.toBeFunction() - // Missing server implementation + // Missing server implementation const noClientImpl = createIsomorphicFn() - .$withType<(a: string) => string>() - .server((a) => 'data') + .$withType<(a: string) => string>() + .server((a) => 'data') expectTypeOf(noClientImpl).not.toBeFunction() const _invalidFnReturn = createIsomorphicFn() From b3b9da94d373fa3a9e3aa6480edb888ddf1307aa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 26 Oct 2025 21:31:33 -0700 Subject: [PATCH 4/6] doc --- .../react/guide/environment-functions.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/start/framework/react/guide/environment-functions.md b/docs/start/framework/react/guide/environment-functions.md index f472a3a26a3..886d21059a1 100644 --- a/docs/start/framework/react/guide/environment-functions.md +++ b/docs/start/framework/react/guide/environment-functions.md @@ -83,6 +83,28 @@ A no-op (short for "no operation") is a function that does nothing when executed function noop() {} ``` +### Restricting the type of the implementations + +You can use `.$withType()` to enforce the type of both the server and client implementations matches the same function signature. + +```tsx +import { createIsomorphicFn } from '@tanstack/react-start' +import type { Environment } from '@/types/environment' + +const getEnv = createIsomorphicFn() + .$withType<() => Environment>() + .server(() => 'server') + .client(() => 'client') + +const env = getEnv() +// ^? Environment +``` + +You will get a type error if either implementation mismatches from the function signature passed to `$withType`. + +> [!NOTE] +> When using `$withType()`, TypeScript will enforce that you define both the server and client implementations. + --- ## `env`Only Functions From 930fb148076b09cf88b59f92f45e483102490b00 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 26 Oct 2025 21:37:43 -0700 Subject: [PATCH 5/6] named types --- .../src/createIsomorphicFn.ts | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-client-core/src/createIsomorphicFn.ts index 3685b6805d9..244bc407737 100644 --- a/packages/start-client-core/src/createIsomorphicFn.ts +++ b/packages/start-client-core/src/createIsomorphicFn.ts @@ -22,31 +22,25 @@ export interface ClientOnlyFn, TClient> } type AnyFn = (...args: Array) => any -export interface ServerOnlyFnWithType, TReturnType> - extends IsomorphicFn { +export interface ClientImplRequired, TReturnType> { client: ( clientImpl: (...args: TArgs) => TReturnType, ) => IsomorphicFn } -export interface ClientOnlyFnWithType, TReturnType> - extends IsomorphicFn { +export interface ServerImplRequired, TReturnType> { server: ( serverImpl: (...args: TArgs) => TReturnType, ) => IsomorphicFn } export interface IsomorphicFnWithType, TReturnType> { - server: (serverImpl: (...args: TArgs) => TReturnType) => { - client: ( - clientImpl: (...args: TArgs) => TReturnType, - ) => IsomorphicFn - } - client: (clientImpl: (...args: TArgs) => TReturnType) => { - server: ( - serverImpl: (...args: TArgs) => TReturnType, - ) => IsomorphicFn - } + server: ( + serverImpl: (...args: TArgs) => TReturnType, + ) => ClientImplRequired + client: ( + clientImpl: (...args: TArgs) => TReturnType, + ) => ServerImplRequired } export interface IsomorphicFnBase extends IsomorphicFn { From 0144e3c421a283939731e0d7f186b40ce28392b2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 26 Oct 2025 21:38:19 -0700 Subject: [PATCH 6/6] fmt --- packages/start-client-core/src/createIsomorphicFn.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/start-client-core/src/createIsomorphicFn.ts b/packages/start-client-core/src/createIsomorphicFn.ts index 244bc407737..4aa508f6e89 100644 --- a/packages/start-client-core/src/createIsomorphicFn.ts +++ b/packages/start-client-core/src/createIsomorphicFn.ts @@ -21,7 +21,6 @@ export interface ClientOnlyFn, TClient> ) => IsomorphicFn } -type AnyFn = (...args: Array) => any export interface ClientImplRequired, TReturnType> { client: ( clientImpl: (...args: TArgs) => TReturnType, @@ -44,10 +43,9 @@ export interface IsomorphicFnWithType, TReturnType> { } export interface IsomorphicFnBase extends IsomorphicFn { - $withType: () => IsomorphicFnWithType< - Parameters, - ReturnType - > + $withType: < + TFn extends (...args: Array) => any, + >() => IsomorphicFnWithType, ReturnType> server: , TServer>( serverImpl: (...args: TArgs) => TServer, ) => ServerOnlyFn