diff --git a/index.d.ts b/index.d.ts index aa48c653d..73ac6ece6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -64,8 +64,9 @@ export type {MultidimensionalReadonlyArray} from './source/multidimensional-read export type {IterableElement} from './source/iterable-element.d.ts'; export type {Entry} from './source/entry.d.ts'; export type {Entries} from './source/entries.d.ts'; -export type {SetReturnType} from './source/set-return-type.d.ts'; +export type {FunctionOverloads} from './source/function-overloads.d.ts'; export type {SetParameterType} from './source/set-parameter-type.d.ts'; +export type {SetReturnType} from './source/set-return-type.d.ts'; export type {Asyncify} from './source/asyncify.d.ts'; export type {Simplify} from './source/simplify.d.ts'; export type {SimplifyDeep} from './source/simplify-deep.d.ts'; diff --git a/readme.md b/readme.md index c3006aff5..40a8b0d57 100644 --- a/readme.md +++ b/readme.md @@ -145,8 +145,6 @@ Click the type names for complete docs. - [`IterableElement`](source/iterable-element.d.ts) - Get the element type of an `Iterable`/`AsyncIterable`. For example, `Array`, `Set`, `Map`, generator, stream, etc. - [`Entry`](source/entry.d.ts) - Create a type that represents the type of an entry of a collection. - [`Entries`](source/entries.d.ts) - Create a type that represents the type of the entries of a collection. -- [`SetReturnType`](source/set-return-type.d.ts) - Create a function type with a return type of your choice and the same parameters as the given function type. -- [`SetParameterType`](source/set-parameter-type.d.ts) - Create a function that replaces some parameters with the given parameters. - [`Simplify`](source/simplify.d.ts) - Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. - [`SimplifyDeep`](source/simplify-deep.d.ts) - Deeply simplifies an object type. - [`Get`](source/get.d.ts) - Get a deeply-nested property from an object using a key path, like [Lodash's `.get()`](https://lodash.com/docs/latest#get) function. @@ -233,6 +231,12 @@ Click the type names for complete docs. - [`AsyncReturnType`](source/async-return-type.d.ts) - Unwrap the return type of a function that returns a `Promise`. - [`Asyncify`](source/asyncify.d.ts) - Create an async version of the given function type. +### Function + +- [`FunctionOverloads`](source/function-overloads.d.ts) - Create a union of all the function's overloads. +- [`SetParameterType`](source/set-parameter-type.d.ts) - Create a function that replaces some parameters with the given parameters. +- [`SetReturnType`](source/set-return-type.d.ts) - Create a function type with a return type of your choice and the same parameters as the given function type. + ### String - [`Trim`](source/trim.d.ts) - Remove leading and trailing spaces from a string. diff --git a/source/function-overloads.d.ts b/source/function-overloads.d.ts new file mode 100644 index 000000000..319a7e655 --- /dev/null +++ b/source/function-overloads.d.ts @@ -0,0 +1,97 @@ +import type {FunctionWithMaybeThisParameter, Not} from './internal/index.d.ts'; +import type {IsEqual} from './is-equal.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; + +/** +Create a union of all the function's overloads. + +TypeScript's built-in utility types like `Parameters` and `ReturnType` only work with the last overload signature, [by design](https://github.com/microsoft/TypeScript/issues/32164). This type extracts all overload signatures as a union, allowing you to work with each overload individually. + +Use-cases: +- Extract parameter types from specific overloads using `Extract` and `Parameters` +- Analyze all possible function signatures in type-level code +- Extract event handler signatures from framework APIs + +Known limitions: +- Functions that have identical parameters but different `this` types or return types will only extract one overload (the last one) +- Generic type parameters are lost and inferred as `unknown` + +@example +```ts +import type {FunctionOverloads} from 'type-fest'; + +function request(url: string): Promise; +function request(url: string, options: {json: true}): Promise; +function request(url: string, options?: {json?: boolean}) { + // ... +} + +type RequestFunctionType = FunctionOverloads; +//=> ((url: string) => Promise) | ((url: string, options: {json: true}) => Promise) + +// You can also get all parameters and return types using built-in `Parameters` and `ReturnType` utilities: + +type RequestParameters = Parameters; +//=> [url: string, options: {json: true}] | [url: string] + +type RequestReturnType = ReturnType; +//=> Promise | Promise +``` + +This type can also be used to extract event parameter types from framework emit functions: + +@example +```ts +// Given a Vue component that defines its events: +defineEmits<{ + submit: [formData: FormData]; + cancel: []; +}>(); + +// Extract the parameter types of the `submit` event: +import type {ArrayTail, FunctionOverloads} from 'type-fest'; +import HelloWorld from './HelloWorld.vue'; + +type SubmitEventType = ArrayTail['$emit']>, (event: 'submit', ...arguments_: readonly any[]) => void>>>; +//=> [formData: FormData] +``` + +@category Function +*/ +export type FunctionOverloads = FunctionOverloadsInternal; + +type FunctionOverloadsInternal< + AllOverloads, + CheckedOverloads = {}, + MustStopIfParametersAreEqual extends boolean = true, + LastParameters = never, +> = AllOverloads extends ( + this: infer ThisType, + ...arguments_: infer ParametersType extends UnknownArray +) => infer ReturnType + ? // This simultaneously checks if the last and the current parameters are equal and `MustStopIfParametersAreEqual` flag is true + IsEqual< + [LastParameters, true], + [ + ParametersType, + [MustStopIfParametersAreEqual] extends [true] ? true : false, // Prevents distributivity + ] + > extends true + ? never + : + | FunctionOverloadsInternal< + // Normally, in `(FunctionType extends (...args: infer P) => infer R)`, compiler infers + // `P` and `R` from the last function overload. + // This trick (intersecting one of the function signatures with the full signature) + // makes compiler infer a last overload that do not equal one of the concatenated ones. + // Thus, we're ending up iterating over all the overloads from bottom to top. + // Credits: https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709 + CheckedOverloads & AllOverloads, + CheckedOverloads & ((this: ThisType, ...arguments_: ParametersType) => ReturnType), + Not, + ParametersType + > + | FunctionWithMaybeThisParameter + : never; + +export {}; diff --git a/source/internal/function.d.ts b/source/internal/function.d.ts new file mode 100644 index 000000000..4b6ab1340 --- /dev/null +++ b/source/internal/function.d.ts @@ -0,0 +1,30 @@ +import type {IsUnknown} from '../is-unknown.d.ts'; + +/** +Constructs a function type with an optional `this` parameter. + +If `this` parameter is not needed, pass `unknown`. + +@example +```ts +type Fun = FunctionWithMaybeThisParameter; +//=> (args_0: string) => void; +``` + +@example +```ts +type Fun = FunctionWithMaybeThisParameter; +//=> (this: object, foo: number, ...bar: string[]) => boolean; +``` +*/ +export type FunctionWithMaybeThisParameter< + ThisParameter, + Parameters_ extends readonly unknown[], + TypeToReturn, +> = + // If a function does not specify the `this` parameter, it will be inferred to `unknown` + IsUnknown extends true + ? (...args: Parameters_) => TypeToReturn + : (this: ThisParameter, ...args: Parameters_) => TypeToReturn; + +export {}; diff --git a/source/internal/index.d.ts b/source/internal/index.d.ts index 864db2d97..c318d3ea3 100644 --- a/source/internal/index.d.ts +++ b/source/internal/index.d.ts @@ -1,5 +1,6 @@ export type * from './array.d.ts'; export type * from './characters.d.ts'; +export type * from './function.d.ts'; export type * from './keys.d.ts'; export type * from './numeric.d.ts'; export type * from './object.d.ts'; diff --git a/source/set-parameter-type.d.ts b/source/set-parameter-type.d.ts index aa00dbd0a..a3db9c57a 100644 --- a/source/set-parameter-type.d.ts +++ b/source/set-parameter-type.d.ts @@ -1,5 +1,4 @@ -import type {IsUnknown} from './is-unknown.d.ts'; -import type {StaticPartOfArray, VariablePartOfArray} from './internal/index.d.ts'; +import type {FunctionWithMaybeThisParameter, StaticPartOfArray, VariablePartOfArray} from './internal/index.d.ts'; import type {UnknownArray} from './unknown-array.d.ts'; /** @@ -104,16 +103,13 @@ type HandleLog2 = SetParameterType; @category Function */ -export type SetParameterType unknown, P extends Record> = - // Just using `Parameters` isn't ideal because it doesn't handle the `this` fake parameter. - Function_ extends (this: infer ThisArgument, ...arguments_: infer Arguments) => unknown - ? ( - // If a function did not specify the `this` fake parameter, it will be inferred to `unknown`. - // We want to detect this situation just to display a friendlier type upon hovering on an IntelliSense-powered IDE. - IsUnknown extends true - ? (...arguments_: MergeObjectToArray) => ReturnType - : (this: ThisArgument, ...arguments_: MergeObjectToArray) => ReturnType - ) - : Function_; // This part should be unreachable +export type SetParameterType< + Function_ extends (...arguments_: any[]) => unknown, + P extends Record, +> = FunctionWithMaybeThisParameter< + ThisParameterType, + MergeObjectToArray, P>, + ReturnType +>; export {}; diff --git a/source/set-return-type.d.ts b/source/set-return-type.d.ts index fdfc83e82..f68947028 100644 --- a/source/set-return-type.d.ts +++ b/source/set-return-type.d.ts @@ -1,4 +1,4 @@ -import type {IsUnknown} from './is-unknown.d.ts'; +import type {FunctionWithMaybeThisParameter} from './internal/index.d.ts'; /** Create a function type with a return type of your choice and the same parameters as the given function type. @@ -17,15 +17,13 @@ type MyWrappedFunction = SetReturnType any, TypeToReturn> = - // Just using `Parameters` isn't ideal because it doesn't handle the `this` fake parameter. - Function_ extends (this: infer ThisArgument, ...arguments_: infer Arguments) => any ? ( - // If a function did not specify the `this` fake parameter, it will be inferred to `unknown`. - // We want to detect this situation just to display a friendlier type upon hovering on an IntelliSense-powered IDE. - IsUnknown extends true ? (...arguments_: Arguments) => TypeToReturn : (this: ThisArgument, ...arguments_: Arguments) => TypeToReturn - ) : ( - // This part should be unreachable, but we make it meaningful just in case… - (...arguments_: Parameters) => TypeToReturn - ); +export type SetReturnType< + Function_ extends (...arguments_: any[]) => any, + TypeToReturn, +> = FunctionWithMaybeThisParameter< + ThisParameterType, + Parameters, + TypeToReturn +>; export {}; diff --git a/test-d/function-overloads.ts b/test-d/function-overloads.ts new file mode 100644 index 000000000..ba9a7feb3 --- /dev/null +++ b/test-d/function-overloads.ts @@ -0,0 +1,109 @@ +import {expectType} from 'tsd'; +import type {FunctionOverloads} from '../source/function-overloads.d.ts'; + +type Function1 = (foo: string, bar: number) => object; +type Function2 = (foo: bigint, ...bar: any[]) => void; + +declare const normalFunction: FunctionOverloads; +expectType(normalFunction); + +// Note: function overload is equivalent to intersecting its signatures + +declare const twoOverloads: FunctionOverloads; +expectType(twoOverloads); + +declare const twoIdenticalOverloads: FunctionOverloads; +expectType(twoIdenticalOverloads); + +type Function3 = (foo: string, bar: number, baz?: boolean) => object; + +declare const twoOverloadsWithAssignableSignature: FunctionOverloads; +expectType(twoOverloadsWithAssignableSignature); + +declare const threeOverloads: FunctionOverloads; +expectType(threeOverloads); + +type Function4 = (...foo: any[]) => void; +type Function5 = (...foo: readonly any[]) => void; + +declare const normalFunctionWithOnlyRestWritableParameter: FunctionOverloads; +expectType(normalFunctionWithOnlyRestWritableParameter); + +declare const normalFunctionWithOnlyRestReadonlyParameter: FunctionOverloads; +expectType(normalFunctionWithOnlyRestReadonlyParameter); + +declare const twoOverloadsWithDifferentRestParameterReadonliness: FunctionOverloads< + Function4 & Function5 +>; +// Expected: it seems like the compiler ignores subsequent identical up to `readonly` modifier overloads +expectType(twoOverloadsWithDifferentRestParameterReadonliness); + +declare const twoOverloadsWithDifferentRestParameterReadonlinessReversed: FunctionOverloads< + Function5 & Function4 +>; +// Expected: it seems like the compiler ignores subsequent identical up to `readonly` modifier overloads +expectType(twoOverloadsWithDifferentRestParameterReadonlinessReversed); + +type Function6 = (foo: string, ...bar: any[]) => void; +type Function7 = (foo: string, ...bar: readonly any[]) => void; + +declare const normalFunctionWithNormalAndRestWritableParameter: FunctionOverloads; +expectType(normalFunctionWithNormalAndRestWritableParameter); + +declare const normalFunctionWithNormalAndRestReadonlyParameter: FunctionOverloads; +// Expected: readonly rest parameter cannot be represented with tuples +expectType<(foo: string, ...bar: any[]) => void>(normalFunctionWithNormalAndRestReadonlyParameter); + +type Function8 = () => never; + +declare const normalFunctionNoParameters: FunctionOverloads; +expectType(normalFunctionNoParameters); + +declare const twoOverloadsWithNoAndPresentParameters: FunctionOverloads; +expectType(twoOverloadsWithNoAndPresentParameters); + +type Function9 = (event: 'event9', arg: string) => void; +type Function10 = (event: 'event10', arg: number) => string; +type Function11 = (event: 'event11', arg: boolean) => never; +type Function12 = (event: 'event12', arg: bigint) => object; + +declare const manyOverloads: FunctionOverloads< + Function1 & + Function2 & + Function3 & + Function4 & + Function5 & + Function6 & + Function7 & + Function8 & + Function9 & + Function10 & + Function11 & + Function12 +>; +expectType< + | Function1 + | Function2 + | Function3 + | Function4 + | Function5 + | Function6 + | Function7 + | Function8 + | Function9 + | Function10 + | Function11 + | Function12 +>(manyOverloads); + +declare const noOverloads: FunctionOverloads<{}>; +expectType(noOverloads); + +declare const anyOverload: FunctionOverloads; +expectType<(...arguments_: readonly unknown[]) => unknown>(anyOverload); + +declare const neverOverload: FunctionOverloads; +expectType(neverOverload); + +declare const unknownOverload: FunctionOverloads; +expectType(unknownOverload);