diff --git a/source/internal/function.d.ts b/source/internal/function.d.ts new file mode 100644 index 000000000..a9db9470c --- /dev/null +++ b/source/internal/function.d.ts @@ -0,0 +1,30 @@ +import type {IsUnknown} from '../is-unknown.d.ts'; + +/** +Test if the given function has multiple call signatures. + +Needed to handle the case of a single call signature with properties. + +Multiple call signatures cannot currently be supported due to a TypeScript limitation. +@see https://github.com/microsoft/TypeScript/issues/29732 +*/ +export type HasMultipleCallSignatures unknown> = + T extends {(...arguments_: infer A): unknown; (...arguments_: infer B): unknown} + ? B extends A + ? A extends B + ? false + : true + : true + : false; + +/** +Extract the call signature of an object type. +*/ +export type ExtractCallSignature = + Type extends (this: infer This, ...args: infer Parameters) => infer Return + ? IsUnknown extends true // TODO: replace with `FunctionWithMaybeThisParameter` + ? (...args: Parameters) => Return + : (this: This, ...args: Parameters) => Return + : unknown; + +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/internal/type.d.ts b/source/internal/type.d.ts index 24c8baa16..5aa667d3f 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -22,23 +22,6 @@ export type IsBothExtends = FirstType extends B : false : false; -/** -Test if the given function has multiple call signatures. - -Needed to handle the case of a single call signature with properties. - -Multiple call signatures cannot currently be supported due to a TypeScript limitation. -@see https://github.com/microsoft/TypeScript/issues/29732 -*/ -export type HasMultipleCallSignatures unknown> = - T extends {(...arguments_: infer A): unknown; (...arguments_: infer B): unknown} - ? B extends A - ? A extends B - ? false - : true - : true - : false; - /** Returns a boolean for whether the given `boolean` is not `false`. */ diff --git a/source/partial-deep.d.ts b/source/partial-deep.d.ts index d1b634dc1..84c8284d7 100644 --- a/source/partial-deep.d.ts +++ b/source/partial-deep.d.ts @@ -1,4 +1,5 @@ import type {ApplyDefaultOptions, BuiltIns, HasMultipleCallSignatures} from './internal/index.d.ts'; +import type {Simplify} from './simplify.d.ts'; import type {IsNever} from './is-never.d.ts'; /** @@ -148,8 +149,8 @@ type PartialReadonlySetDeep> = { /** Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`. */ -type PartialObjectDeep> = { +type PartialObjectDeep> = Simplify<{ [KeyType in keyof ObjectType]?: _PartialDeep -}; +}>; export {}; diff --git a/source/required-deep.d.ts b/source/required-deep.d.ts index 0c91744a7..dc88801b1 100644 --- a/source/required-deep.d.ts +++ b/source/required-deep.d.ts @@ -1,4 +1,5 @@ import type {BuiltIns, HasMultipleCallSignatures} from './internal/index.d.ts'; +import type {Simplify} from './simplify.d.ts'; import type {IsNever} from './is-never.d.ts'; /** @@ -68,8 +69,8 @@ export type RequiredDeep = T extends BuiltIns ? RequiredObjectDeep : unknown; -type RequiredObjectDeep = { +type RequiredObjectDeep = Simplify<{ [KeyType in keyof ObjectType]-?: RequiredDeep -}; +}>; export {}; diff --git a/source/set-optional.d.ts b/source/set-optional.d.ts index 86dfbb324..5ff97c161 100644 --- a/source/set-optional.d.ts +++ b/source/set-optional.d.ts @@ -1,6 +1,6 @@ -import type {Except} from './except.d.ts'; -import type {HomomorphicPick} from './internal/index.d.ts'; +import type {ExtractCallSignature, HomomorphicPick} from './internal/index.d.ts'; import type {Simplify} from './simplify.d.ts'; +import type {Except} from './except.d.ts'; /** Create a type that makes the given keys optional. The remaining keys are kept as is. The sister of the `SetRequired` type. @@ -28,19 +28,16 @@ type SomeOptional = SetOptional; @category Object */ export type SetOptional = - (BaseType extends (...arguments_: never) => any - ? (...arguments_: Parameters) => ReturnType - : unknown) - & _SetOptional; + Simplify & _SetOptional>; type _SetOptional = BaseType extends unknown // To distribute `BaseType` when it's a union type. - ? Simplify< - // Pick just the keys that are readonly from the base type. + ? ( + // Pick just the keys that are readonly from the base type. Except & - // Pick the keys that should be mutable from the base type and make them mutable. + // Pick the keys that should be mutable from the base type and make them mutable. Partial> - > + ) : never; export {}; diff --git a/source/set-readonly.d.ts b/source/set-readonly.d.ts index 39d58d364..694a2d656 100644 --- a/source/set-readonly.d.ts +++ b/source/set-readonly.d.ts @@ -1,6 +1,6 @@ -import type {Except} from './except.d.ts'; -import type {HomomorphicPick} from './internal/index.d.ts'; +import type {ExtractCallSignature, HomomorphicPick} from './internal/index.d.ts'; import type {Simplify} from './simplify.d.ts'; +import type {Except} from './except.d.ts'; /** Create a type that makes the given keys readonly. The remaining keys are kept as is. @@ -28,17 +28,16 @@ type SomeReadonly = SetReadonly; @category Object */ export type SetReadonly = - (BaseType extends (...arguments_: never) => any - ? (...arguments_: Parameters) => ReturnType - : unknown) - & _SetReadonly; + Simplify & _SetReadonly>; export type _SetReadonly = BaseType extends unknown // To distribute `BaseType` when it's a union type. - ? Simplify< + ? ( + // Pick just the keys that are writable from the base type. Except & + // Pick the keys that should be readonly from the base type and make them readonly. Readonly> - > + ) : never; export {}; diff --git a/source/set-required.d.ts b/source/set-required.d.ts index 4a6b2fba5..cb3f20390 100644 --- a/source/set-required.d.ts +++ b/source/set-required.d.ts @@ -1,9 +1,9 @@ +import type {ExtractCallSignature, HomomorphicPick, IsArrayReadonly} from './internal/index.d.ts'; +import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; +import type {Simplify} from './simplify.d.ts'; import type {Except} from './except.d.ts'; import type {If} from './if.d.ts'; -import type {HomomorphicPick, IsArrayReadonly} from './internal/index.d.ts'; -import type {OptionalKeysOf} from './optional-keys-of.d.ts'; -import type {Simplify} from './simplify.d.ts'; -import type {UnknownArray} from './unknown-array.d.ts'; /** Create a type that makes the given keys required. The remaining keys are kept as is. The sister of the `SetOptional` type. @@ -35,22 +35,19 @@ type ArrayExample = SetRequired<[number?, number?, number?], 0 | 1>; @category Object */ export type SetRequired = - (BaseType extends (...arguments_: never) => any - ? (...arguments_: Parameters) => ReturnType - : unknown) - & _SetRequired; + Simplify & _SetRequired>; type _SetRequired = BaseType extends UnknownArray ? SetArrayRequired extends infer ResultantArray ? If, Readonly, ResultantArray> : never - : Simplify< - // Pick just the keys that are optional from the base type. + : ( + // Pick just the keys that are optional from the base type. Except & - // Pick the keys that should be required from the base type and make them required. + // Pick the keys that should be required from the base type and make them required. Required> - >; + ); /** Remove the optional modifier from the specified keys in an array. @@ -66,7 +63,7 @@ type SetArrayRequired< // `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`). ? [...Accumulator, ...TArray] : TArray extends readonly [(infer First)?, ...infer Rest] - ? '0' extends OptionalKeysOf // If the first element of `TArray` is optional + ? IsOptionalKeyOf extends true // If the first element of `TArray` is optional ? `${Counter['length']}` extends `${Keys & (string | number)}` // If the current index needs to be required ? SetArrayRequired // If the current element is optional, but it doesn't need to be required, diff --git a/source/simplify.d.ts b/source/simplify.d.ts index a84d03970..10f2fefe0 100644 --- a/source/simplify.d.ts +++ b/source/simplify.d.ts @@ -1,3 +1,5 @@ +import type {ExtractCallSignature} from './internal/function.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. @@ -55,6 +57,11 @@ fn(someInterface as Simplify); // Good: transform an `interface` @see {@link SimplifyDeep} @category Object */ -export type Simplify = {[KeyType in keyof T]: T[KeyType]} & {}; +export type Simplify = + Type extends unknown + ? ExtractCallSignature & _Simplify // TODO: change to `(_Simplify extends {} & infer U ? U : never)` + : never; + +type _Simplify = {[K in keyof T]: T[K]} & {}; export {}; diff --git a/test-d/partial-deep.ts b/test-d/partial-deep.ts index 93baa52a5..325911c44 100644 --- a/test-d/partial-deep.ts +++ b/test-d/partial-deep.ts @@ -114,24 +114,24 @@ expectType(partialDeepNoRecurseIntoArraysBar.reado type FunctionWithProperties = {(a1: string, a2: number): boolean; p1: string; readonly p2: number}; declare const functionWithProperties: PartialDeep; expectType(functionWithProperties('foo', 1)); -expectType<{p1?: string; readonly p2?: number}>({} as Simplify); // `Simplify` removes the call signature from `typeof functionWithProperties` +expectType<((a1: string, a2: number) => boolean) & {p1?: string; readonly p2?: number}>(functionWithProperties); type FunctionWithProperties2 = {(a1: boolean, ...a2: string[]): number; p1: {p2?: string; p3: {readonly p4?: boolean}}}; declare const functionWithProperties2: PartialDeep; expectType(functionWithProperties2(true, 'foo', 'bar')); -expectType<{p1?: {p2?: string; p3?: {readonly p4?: boolean}}}>({} as Simplify); +expectType<((a1: boolean, ...a2: string[]) => number) & {p1?: {p2?: string; p3?: {readonly p4?: boolean}}}>(functionWithProperties2); type FunctionWithProperties3 = {(): void; p1: {p2?: string; p3: [{p4: number}, string]}}; declare const functionWithProperties3: PartialDeep; expectType(functionWithProperties3()); -expectType<{p1?: {p2?: string; p3?: [{p4?: number}?, string?]}}>({} as Simplify); +expectType<(() => void) & {p1?: {p2?: string; p3?: [{p4?: number}?, string?]}}>(functionWithProperties3); -expectType<{p1?: string[]}>({} as Simplify>); -expectType<{p1?: string[]}>({} as Simplify>); +expectType<(() => void) & {p1?: string[]}>({} as PartialDeep<{(): void; p1: string[]}, {allowUndefinedInNonTupleArrays: false}>); +expectType<(() => void) & {p1?: string[]}>({} as PartialDeep<{(): void; p1: string[]}, {allowUndefinedInNonTupleArrays: true}>); // Properties within functions containing multiple call signatures are not made partial due to TS limitations, refer https://github.com/microsoft/TypeScript/issues/29732 type FunctionWithProperties4 = {(a1: number): string; (a1: string, a2: number): number; p1: string}; declare const functionWithProperties4: PartialDeep; expectType(functionWithProperties4(1)); expectType(functionWithProperties4('foo', 1)); -expectNotType<{p1?: string}>({} as Simplify); +expectNotType<((a1: number) => string) & ((a1: string, a2: number) => number) & {p1?: string}>(functionWithProperties4); diff --git a/test-d/required-deep.ts b/test-d/required-deep.ts index 1165d3e51..fbfbcec28 100644 --- a/test-d/required-deep.ts +++ b/test-d/required-deep.ts @@ -65,21 +65,21 @@ expectType>>>( type FunctionWithProperties = {(a1: string, a2: number): boolean; p1?: string; readonly p2?: number}; declare const functionWithProperties: RequiredDeep; expectType(functionWithProperties('foo', 1)); -expectType<{p1: string; readonly p2: number}>({} as Simplify); // `Simplify` removes the call signature from `typeof functionWithProperties` +expectType<((a1: string, a2: number) => boolean) & {p1: string; readonly p2: number}>(functionWithProperties); type FunctionWithProperties2 = {(a1: boolean, ...a2: string[]): number; p1?: {p2?: string; p3: {readonly p4?: boolean}}}; declare const functionWithProperties2: RequiredDeep; expectType(functionWithProperties2(true, 'foo', 'bar')); -expectType<{p1: {p2: string; p3: {readonly p4: boolean}}}>({} as Simplify); +expectType<((a1: boolean, ...a2: string[]) => number) & {p1: {p2: string; p3: {readonly p4: boolean}}}>(functionWithProperties2); type FunctionWithProperties3 = {(): void; p1?: {p2?: string; p3: [{p4?: number}, string?]}}; declare const functionWithProperties3: RequiredDeep; expectType(functionWithProperties3()); -expectType<{p1: {p2: string; p3: [{p4: number}, string]}}>({} as Simplify); +expectType<(() => void) & {p1: {p2: string; p3: [{p4: number}, string]}}>(functionWithProperties3); // Properties within functions containing multiple call signatures are not made required due to TS limitations, refer https://github.com/microsoft/TypeScript/issues/29732 type FunctionWithProperties4 = {(a1: number): string; (a1: string, a2: number): number; p1?: string}; declare const functionWithProperties4: RequiredDeep; expectType(functionWithProperties4(1)); expectType(functionWithProperties4('foo', 1)); -expectNotType<{p1: string}>({} as Simplify); +expectNotType<((a1: number) => string) & ((a1: string, a2: number) => number) & {p1: string}>(functionWithProperties4); diff --git a/test-d/set-optional.ts b/test-d/set-optional.ts index ab67f76de..8250dc659 100644 --- a/test-d/set-optional.ts +++ b/test-d/set-optional.ts @@ -1,5 +1,5 @@ import {expectNotAssignable, expectType} from 'tsd'; -import type {SetOptional, Simplify} from '../index.d.ts'; +import type {SetOptional} from '../index.d.ts'; // Update one required and one optional to optional. declare const variation1: SetOptional<{a: number; b?: string; c: boolean}, 'b' | 'c'>; @@ -40,11 +40,11 @@ expectType<{[k: string]: unknown; a?: number; b?: string}>(variation9); // Works with functions containing properties declare const variation10: SetOptional<{(a1: string, a2: number): boolean; p1: string; readonly p2?: number}, 'p1'>; expectType(variation10('foo', 1)); -expectType<{p1?: string; readonly p2?: number}>({} as Simplify); // `Simplify` removes the call signature from `typeof variation10` +expectType<((a1: string, a2: number) => boolean) & {p1?: string; readonly p2?: number}>(variation10); declare const variation11: SetOptional<{(a1: boolean, ...a2: string[]): number; p1: string; readonly p2: number; p3: boolean}, 'p1' | 'p2'>; expectType(variation11(true, 'foo', 'bar', 'baz')); -expectType<{p1?: string; readonly p2?: number; p3: boolean}>({} as Simplify); +expectType<((a1: boolean, ...a2: string[]) => number) & {p1?: string; readonly p2?: number; p3: boolean}>(variation11); // Functions without properties are returned as is declare const variation12: SetOptional<(a: string) => number, never>; diff --git a/test-d/set-readonly.ts b/test-d/set-readonly.ts index 72174c287..bf6208e1b 100644 --- a/test-d/set-readonly.ts +++ b/test-d/set-readonly.ts @@ -1,5 +1,5 @@ import {expectNotAssignable, expectType} from 'tsd'; -import type {SetReadonly, Simplify} from '../index.d.ts'; +import type {SetReadonly} from '../index.d.ts'; // Update one readonly and one non readonly to readonly. declare const variation1: SetReadonly<{a: number; readonly b: string; c: boolean}, 'b' | 'c'>; @@ -40,11 +40,11 @@ expectType<{[k: string]: unknown; readonly a: number; readonly b: string}>(varia // Works with functions containing properties declare const variation10: SetReadonly<{(a1: string, a2: number): boolean; p1: string; readonly p2?: number}, 'p1'>; expectType(variation10('foo', 1)); -expectType<{readonly p1: string; readonly p2?: number}>({} as Simplify); // Simplify removes the call signature from `typeof variation10` +expectType<((a1: string, a2: number) => boolean) & {readonly p1: string; readonly p2?: number}>(variation10); declare const variation11: SetReadonly<{(a1: boolean, ...a2: string[]): number; p1: string; p2?: number; p3: boolean}, 'p1' | 'p2'>; expectType(variation11(true, 'foo', 'bar', 'baz')); -expectType<{readonly p1: string; readonly p2?: number; p3: boolean}>({} as Simplify); +expectType<((a1: boolean, ...a2: string[]) => number) & {readonly p1: string; readonly p2?: number; p3: boolean}>(variation11); // Functions without properties are returned as is declare const variation12: SetReadonly<(a: string) => number, never>; diff --git a/test-d/set-required.ts b/test-d/set-required.ts index 809285f11..bdd57ee48 100644 --- a/test-d/set-required.ts +++ b/test-d/set-required.ts @@ -1,5 +1,5 @@ import {expectNotAssignable, expectType} from 'tsd'; -import type {SetRequired, Simplify} from '../index.d.ts'; +import type {SetRequired} from '../index.d.ts'; // Update one required and one optional to required. declare const variation1: SetRequired<{a?: number; b: string; c?: boolean}, 'b' | 'c'>; @@ -44,11 +44,11 @@ expectType<{[k: string]: unknown; a: number; b: string}>(variation10); // Works with functions containing properties declare const variation11: SetRequired<{(a1: string, a2: number): boolean; p1?: string; readonly p2: number}, 'p1'>; expectType(variation11('foo', 1)); -expectType<{p1: string; readonly p2: number}>({} as Simplify); +expectType<((a1: string, a2: number) => boolean) & {p1: string; readonly p2: number}>(variation11); declare const variation12: SetRequired<{(a1: boolean, ...a2: string[]): number; p1?: string; readonly p2?: number; p3?: boolean}, 'p1' | 'p2'>; expectType(variation12(true, 'foo', 'bar', 'baz')); -expectType<{p1: string; readonly p2: number; p3?: boolean}>({} as Simplify); +expectType<((a1: boolean, ...a2: string[]) => number) & {p1: string; readonly p2: number; p3?: boolean}>(variation12); // Functions without properties are returned as is declare const variation13: SetRequired<(a: string) => number, never>; diff --git a/test-d/simplify.ts b/test-d/simplify.ts index 12b9d80c8..4cd557e00 100644 --- a/test-d/simplify.ts +++ b/test-d/simplify.ts @@ -44,32 +44,27 @@ expectAssignable>(valueAsLiteral); expectAssignable>(valueAsSimplifiedInterface); expectNotAssignable>(valueAsInterface); // Index signature is missing in interface -// The following tests should be fixed once we have determined the cause of the bug reported in https://github.com/sindresorhus/type-fest/issues/436 - type SomeFunction = (type: string) => string; -type SimplifiedFunction = Simplify; // Return '{}' expected 'SomeFunction' +type SimplifiedFunction = Simplify; declare const someFunction: SimplifiedFunction; -expectNotAssignable(someFunction); - -// // Should return the original type if it is not simplifiable, like a function. -// type SomeFunction = (type: string) => string; -// expectType>((type: string) => type); +expectType<(SomeFunction & Record)>(someFunction); // TODO: find a way to remove `& {}` without breaking other types. -// class SomeClass { -// id: string; +class SomeClass { + id: string; -// private readonly code: number; + private readonly code: number; -// constructor() { -// this.id = 'some-class'; -// this.code = 42; -// } + constructor() { + this.id = 'some-class'; + this.code = 42; + } -// someMethod() { -// return this.code; -// } -// } + someMethod() { + return this.code; + } +} -// expectType>(new SomeClass()); +expectType>({} as {id: string; someMethod: () => number}); +expectType>({prototype: new SomeClass()});