Skip to content

Commit

Permalink
Add ReadonlyDeep type (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
BendingBender authored and sindresorhus committed May 12, 2019
1 parent 0e0a32c commit 22c3a99
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 1 deletion.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {Mutable} from './source/mutable';
export {Merge} from './source/merge';
export {MergeExclusive} from './source/merge-exclusive';
export {RequireAtLeastOne} from './source/require-at-least-one';
export {ReadonlyDeep} from './source/readonly-deep';
export {LiteralUnion} from './source/literal-union';

// Miscellaneous
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ Click the type names for complete docs.
- [`Merge`](source/merge.d.ts) - Merge two types into a new type. Keys of the second type overrides keys of the first type.
- [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive properties.
- [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given properties.
- [`LiteralUnion`](source/literal-union.d.ts) - Allows creating a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).
- [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of a `object`/`Map`/`Set`/`Array` type.
- [`LiteralUnion`](source/literal-union.d.ts) - Create a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).

### Miscellaneous

Expand Down
59 changes: 59 additions & 0 deletions source/readonly-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Primitive} from './basic';

/**
Convert `object`s, `Map`s, `Set`s, and `Array`s and all of their properties/elements into immutable structures recursively.
This is useful when a deeply nested structure needs to be exposed as completely immutable, for example, an imported JSON module or when receiving an API response that is passed around.
Please upvote [this issue](https://github.com/microsoft/TypeScript/issues/13923) if you want to have this type as a built-in in TypeScript.
@example
```
// data.json
{
"foo": ["bar"]
}
// main.ts
import {ReadonlyDeep} from 'type-fest';
import dataJson = require('./data.json');
const data: ReadonlyDeep<typeof dataJson> = dataJson;
export default data;
// test.ts
import data from './main';
data.foo.push('bar');
//=> error TS2339: Property 'push' does not exist on type 'readonly string[]'
```
*/
export type ReadonlyDeep<T> = T extends Primitive | ((...arguments: any[]) => unknown)
? T
: T extends ReadonlyMap<infer KeyType, infer ValueType>
? ReadonlyMapDeep<KeyType, ValueType>
: T extends ReadonlySet<infer ItemType>
? ReadonlySetDeep<ItemType>
: T extends object
? ReadonlyObjectDeep<T>
: unknown;

/**
Same as `ReadonlyDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `ReadonlyDeep`.
*/
interface ReadonlyMapDeep<KeyType, ValueType>
extends ReadonlyMap<ReadonlyDeep<KeyType>, ReadonlyDeep<ValueType>> {}

/**
Same as `ReadonlyDeep`, but accepts only `ReadonlySet`s as inputs. Internal helper for `ReadonlyDeep`.
*/
interface ReadonlySetDeep<ItemType>
extends ReadonlySet<ReadonlyDeep<ItemType>> {}

/**
Same as `ReadonlyDeep`, but accepts only `object`s as inputs. Internal helper for `ReadonlyDeep`.
*/
type ReadonlyObjectDeep<ObjectType extends object> = {
readonly [PropertyType in keyof ObjectType]: ReadonlyDeep<ObjectType[PropertyType]>
};
44 changes: 44 additions & 0 deletions test-d/readonly-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {expectType, expectError} from 'tsd';
import {ReadonlyDeep} from '../source/readonly-deep';

const data = {
object: {
foo: 'bar'
},
fn: (_: string) => true,
string: 'foo',
number: 1,
boolean: false,
symbol: Symbol('test'),
null: null,
undefined: undefined, // eslint-disable-line object-shorthand
map: new Map<string, string>(),
set: new Set<string>(),
array: ['foo'],
tuple: ['foo'] as ['foo'],
readonlyMap: new Map<string, string>() as ReadonlyMap<string, string>,
readonlySet: new Set<string>() as ReadonlySet<string>,
readonlyArray: ['foo'] as ReadonlyArray<string>,
readonlyTuple: ['foo'] as const
};

const readonlyData: ReadonlyDeep<typeof data> = data;

readonlyData.fn('foo');

expectError((readonlyData.string = 'bar'));
expectType<{readonly foo: string}>(readonlyData.object);
expectType<string>(readonlyData.string);
expectType<number>(readonlyData.number);
expectType<boolean>(readonlyData.boolean);
expectType<symbol>(readonlyData.symbol);
expectType<null>(readonlyData.null);
expectType<undefined>(readonlyData.undefined);
expectType<ReadonlyMap<string, string>>(readonlyData.map);
expectType<ReadonlySet<string>>(readonlyData.set);
expectType<readonly string[]>(readonlyData.array);
expectType<readonly ['foo']>(readonlyData.tuple);
expectType<ReadonlyMap<string, string>>(readonlyData.readonlyMap);
expectType<ReadonlySet<string>>(readonlyData.readonlySet);
expectType<readonly string[]>(readonlyData.readonlyArray);
expectType<readonly ['foo']>(readonlyData.readonlyTuple);

0 comments on commit 22c3a99

Please sign in to comment.