[core/public/deepFreeze] fix recursive type for better array support (#22904)

The `deepFreeze()` function used by `core.injectedMetadata` uses a recursive type definition to indicate that all of the child types within the passed argument become readonly, which works fine for objects but represents arrays as objects instead of using the `ReadonlyArray<>` type. 

This PR fixes the type definition to use a `RecursiveReadonlyArray<>` type that properly represents arrays with their methods like `push()`, and iterates into the array properly to propagate `ReadOnly<>`, as proven by the tests.
This commit is contained in:
Spencer 2018-09-11 08:43:26 -07:00 committed by GitHub
parent 15322e7256
commit 6ce47520ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 41 deletions

View file

@ -17,17 +17,34 @@
* under the License.
*/
import { deepFreeze } from '../deep_freeze';
import { deepFreeze } from '../../deep_freeze';
const obj = deepFreeze({
foo: {
bar: {
baz: 1,
deepFreeze(
{
foo: {
bar: {
baz: 1,
},
},
},
});
}
).foo.bar.baz = 2;
delete obj.foo;
obj.foo = 1;
obj.foo.bar.baz = 2;
obj.foo.bar.box = false;
deepFreeze(
{
foo: [
{
bar: 1,
},
],
}
).foo[0].bar = 2;
deepFreeze(
{
foo: [1],
}
).foo[0] = 2;
deepFreeze({
foo: [1],
}).foo.push(2);

View file

@ -6,8 +6,7 @@
"esnext"
]
},
"include": [
"frozen_object_mutation.ts",
"../deep_freeze.ts"
"files": [
"index.ts"
]
}

View file

@ -75,28 +75,17 @@ it('prevents reassigning items in a frozen array', () => {
});
it('types return values to prevent mutations in typescript', async () => {
const result = await execa.stdout(
'tsc',
[
'--noEmit',
'--project',
resolve(__dirname, '__fixtures__/frozen_object_mutation.tsconfig.json'),
],
{
cwd: resolve(__dirname, '__fixtures__'),
reject: false,
}
);
await expect(
execa.stdout('tsc', ['--noEmit'], {
cwd: resolve(__dirname, '__fixtures__/frozen_object_mutation'),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Command failed: tsc --noEmit
const errorCodeRe = /\serror\s(TS\d{4}):/g;
const errorCodes = [];
while (true) {
const match = errorCodeRe.exec(result);
if (!match) {
break;
}
errorCodes.push(match[1]);
}
expect(errorCodes).toEqual(['TS2704', 'TS2540', 'TS2540', 'TS2339']);
index.ts(30,11): error TS2540: Cannot assign to 'baz' because it is a constant or a read-only property.
index.ts(40,10): error TS2540: Cannot assign to 'bar' because it is a constant or a read-only property.
index.ts(42,1): error TS2542: Index signature in type 'RecursiveReadonlyArray<number>' only permits reading.
index.ts(50,8): error TS2339: Property 'push' does not exist on type 'RecursiveReadonlyArray<number>'.
"
`);
});

View file

@ -19,9 +19,12 @@
type Freezable = { [k: string]: any } | any[];
type RecursiveReadOnly<T> = T extends Freezable
? Readonly<{ [K in keyof T]: RecursiveReadOnly<T[K]> }>
: T;
// if we define this inside RecursiveReadonly TypeScript complains
interface RecursiveReadonlyArray<T> extends ReadonlyArray<RecursiveReadonly<T>> {}
type RecursiveReadonly<T> = T extends any[]
? RecursiveReadonlyArray<T[number]>
: T extends object ? Readonly<{ [K in keyof T]: RecursiveReadonly<T[K]> }> : T;
export function deepFreeze<T extends Freezable>(object: T) {
// for any properties that reference an object, makes sure that object is
@ -32,5 +35,5 @@ export function deepFreeze<T extends Freezable>(object: T) {
}
}
return Object.freeze(object) as RecursiveReadOnly<T>;
return Object.freeze(object) as RecursiveReadonly<T>;
}