Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion source/readonly-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ data.foo.push('bar');
@category Set
@category Map
*/
export type ReadonlyDeep<T> = T extends BuiltIns | ((...arguments: any[]) => unknown)
export type ReadonlyDeep<T> = T extends BuiltIns
? T
: T extends (...arguments: any[]) => unknown
? {} extends ReadonlyObjectDeep<T>
? T
: HasMultipleCallSignatures<T> extends true
? T
: ((...arguments: Parameters<T>) => ReturnType<T>) & ReadonlyObjectDeep<T>
: T extends Readonly<ReadonlyMap<infer KeyType, infer ValueType>>
? ReadonlyMapDeep<KeyType, ValueType>
: T extends Readonly<ReadonlySet<infer ItemType>>
Expand All @@ -62,3 +68,18 @@ Same as `ReadonlyDeep`, but accepts only `object`s as inputs. Internal helper fo
type ReadonlyObjectDeep<ObjectType extends object> = {
readonly [KeyType in keyof ObjectType]: ReadonlyDeep<ObjectType[KeyType]>
};

/**
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
*/
type HasMultipleCallSignatures<T extends (...arguments: any[]) => unknown> =
T extends {(...arguments: infer A): unknown; (...arguments: any[]): unknown}
? unknown[] extends A
? false
: true
: false;
32 changes: 32 additions & 0 deletions test-d/readonly-deep.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import {expectType, expectError} from 'tsd';
import {ReadonlyDeep} from '../index';
import {ReadonlyObjectDeep} from '../source/readonly-deep';

type Overloaded = {
(foo: number): string;
(foo: string, bar: number): number;
};

type Namespace = {
(foo: number): string;
baz: boolean[];
};

type NamespaceWithOverload = Overloaded & {
baz: boolean[];
};

const data = {
object: {
foo: 'bar',
},
fn: (_: string) => true,
fnWithOverload: ((_: number) => 'foo') as Overloaded,
namespace: {} as unknown as Namespace,
namespaceWithOverload: {} as unknown as NamespaceWithOverload,
string: 'foo',
number: 1,
boolean: false,
Expand All @@ -28,6 +46,9 @@ const readonlyData: ReadonlyDeep<typeof data> = data;

readonlyData.fn('foo');

readonlyData.fnWithOverload(1);
readonlyData.fnWithOverload('', 1);

expectError(readonlyData.string = 'bar');
expectType<{readonly foo: string}>(readonlyData.object);
expectType<string>(readonlyData.string);
Expand All @@ -46,3 +67,14 @@ expectType<Readonly<ReadonlyMap<string, string>>>(readonlyData.readonlyMap);
expectType<Readonly<ReadonlySet<string>>>(readonlyData.readonlySet);
expectType<readonly string[]>(readonlyData.readonlyArray);
expectType<readonly ['foo']>(readonlyData.readonlyTuple);

expectType<((foo: number) => string) & ReadonlyObjectDeep<Namespace>>(readonlyData.namespace);
expectType<string>(readonlyData.namespace(1));
expectType<readonly boolean[]>(readonlyData.namespace.baz);

// These currently aren't readonly due to TypeScript limitations.
// @see https://github.com/microsoft/TypeScript/issues/29732
expectType<NamespaceWithOverload>(readonlyData.namespaceWithOverload);
expectType<string>(readonlyData.namespaceWithOverload(1));
expectType<number>(readonlyData.namespaceWithOverload('foo', 1));
expectType<boolean[]>(readonlyData.namespaceWithOverload.baz);