Functions in JavaScript/TypeScript act as callable entities. They possess properties similar to other objects and are considered subtypes of the object
type.
type Chainable<T> = {
to<U extends object>(fnOrObj: ChainFunction<T, U> | U): Chainable<T & U>;
};
In this context, there is no restriction on U
being a function type. For instance, if you define a function f
with the type
(value: {a: number}) => number
, then calling
to(f)
will be successful because TypeScript infers
U
as
(value: {a: number}) => number
. Despite your intention for
U
to be a non-function object, you haven't explicitly communicated that to TypeScript.
Due to TypeScript's lack of negated types, it doesn't directly support a way to specify "not a function". A theoretical approach using negated types might look like:
// This is only an illustration and not valid TypeScript
type NonFunction = object & not Function
type Chainable<T> = {
to<U extends NonFunction>(fnOrObj: ChainFunction<T, U> | U): Chainable<T & U>;
};
To overcome this limitation, one workaround is to define a NonFunction
type as an object without a call
property. Given that all functions have a call
property by default, this method effectively differentiates between function and non-function objects:
type NonFunction = object & { call?: never }
Implementing this solution ensures the desired behavior:
chain({ a: 1 }).to({ b: 3 }); // valid
chain({ a: 1 }).to(({ a }) => ({ c: a })); // valid
chain({ a: 1 }).to(({ a }) => 42); // error
// ~~~~~~~~~~~~~~
// Type 'number' is not assignable to type 'NonFunction | Promise<NonFunction>'
This straightforward approach proves effective, although encountering a {call: any}
scenario could potentially disrupt the functionality. For more advanced methods, refer to the provided link discussing TypeScript generic types excluding functions.
If you wish to outright prohibit functions, another technique involves utilizing conditional types within generics to enforce that U
always represents the type of fnOrObj
. Furthermore, this strategy rejects functions returning primitives and computes the Chainable
type argument from U
:
type Chainable<T> = {
to<U extends object>(fnOrObj:
U extends Function ? ChainFunction<T, object> : U
): Chainable<T & (U extends ChainFunction<T, infer R> ? R : U)>;
};
declare function chain<T extends {}>(initialValue: T): Chainable<T>
const x = chain({ a: 1 }).to({ b: 3 }); // valid
const y = chain({ a: 1 }).to(({ a }) => ({ c: a })); // valid
const z = chain({ a: 1 }).to(({ a }) => 42); // error
This precise method enforces the expected behavior but may appear overly complex for certain scenarios.
Access the code snippet here