Skip to content

Commit a941c72

Browse files
authored
fix(types): Object.freeze type pollution from @sentry-internal/replay (#5408)
* fix(types): `Object.freeze` type pollution from `@sentry-internal/replay` * Update changelog
1 parent ff5a06a commit a941c72

File tree

4 files changed

+140
-1
lines changed

4 files changed

+140
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Fixes
12+
13+
- Fix `Object.freeze` type pollution from `@sentry-internal/replay` ([#5408](https://github.com/getsentry/sentry-react-native/issues/5408))
14+
1115
### Dependencies
1216

1317
- Bump Android SDK from v8.27.0 to v8.27.1 ([#5404](https://github.com/getsentry/sentry-react-native/pull/5404))
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Type test for Object.freeze pollution fix
3+
*
4+
* This test ensures that Object.freeze() resolves to the correct built-in type
5+
* and not to a polluted type from @sentry-internal/replay.
6+
*
7+
* See: https://github.com/getsentry/sentry-react-native/issues/5407
8+
*/
9+
10+
describe('Object.freeze type inference', () => {
11+
it('should correctly freeze plain objects', () => {
12+
const frozenObject = Object.freeze({
13+
key: 'value',
14+
num: 42,
15+
});
16+
17+
// Runtime test: should be frozen
18+
expect(Object.isFrozen(frozenObject)).toBe(true);
19+
20+
// Type test: TypeScript should infer Readonly<{ key: string; num: number; }>
21+
expect(frozenObject.key).toBe('value');
22+
expect(frozenObject.num).toBe(42);
23+
});
24+
25+
it('should correctly freeze objects with as const', () => {
26+
const EVENTS = Object.freeze({
27+
CLICK: 'click',
28+
SUBMIT: 'submit',
29+
} as const);
30+
31+
// Runtime test: should be frozen
32+
expect(Object.isFrozen(EVENTS)).toBe(true);
33+
34+
// Type test: TypeScript should infer literal types
35+
expect(EVENTS.CLICK).toBe('click');
36+
expect(EVENTS.SUBMIT).toBe('submit');
37+
38+
// TypeScript should infer: Readonly<{ CLICK: "click"; SUBMIT: "submit"; }>
39+
const eventType: 'click' | 'submit' = EVENTS.CLICK;
40+
expect(eventType).toBe('click');
41+
});
42+
43+
it('should correctly freeze functions', () => {
44+
const frozenFn = Object.freeze((x: number) => x * 2);
45+
46+
// Runtime test: should be frozen
47+
expect(Object.isFrozen(frozenFn)).toBe(true);
48+
49+
// Type test: function should still be callable
50+
const result: number = frozenFn(5);
51+
expect(result).toBe(10);
52+
});
53+
54+
it('should correctly freeze nested objects', () => {
55+
const ACTIONS = Object.freeze({
56+
USER: Object.freeze({
57+
LOGIN: 'user:login',
58+
LOGOUT: 'user:logout',
59+
} as const),
60+
ADMIN: Object.freeze({
61+
DELETE: 'admin:delete',
62+
} as const),
63+
} as const);
64+
65+
// Runtime test: should be frozen
66+
expect(Object.isFrozen(ACTIONS)).toBe(true);
67+
expect(Object.isFrozen(ACTIONS.USER)).toBe(true);
68+
expect(Object.isFrozen(ACTIONS.ADMIN)).toBe(true);
69+
70+
// Type test: should preserve nested literal types
71+
expect(ACTIONS.USER.LOGIN).toBe('user:login');
72+
expect(ACTIONS.ADMIN.DELETE).toBe('admin:delete');
73+
74+
// TypeScript should infer the correct literal type
75+
const action: 'user:login' = ACTIONS.USER.LOGIN;
76+
expect(action).toBe('user:login');
77+
});
78+
79+
it('should maintain type safety and prevent modifications at compile time', () => {
80+
const frozen = Object.freeze({ value: 42 });
81+
82+
// Runtime: attempting to modify should silently fail (in non-strict mode)
83+
// or throw (in strict mode)
84+
expect(() => {
85+
// @ts-expect-error - TypeScript should prevent this at compile time
86+
(frozen as any).value = 100;
87+
}).not.toThrow(); // In non-strict mode, this silently fails
88+
89+
// Value should remain unchanged
90+
expect(frozen.value).toBe(42);
91+
});
92+
93+
it('should work with array freeze', () => {
94+
const frozenArray = Object.freeze([1, 2, 3]);
95+
96+
// Runtime test
97+
expect(Object.isFrozen(frozenArray)).toBe(true);
98+
expect(frozenArray).toEqual([1, 2, 3]);
99+
100+
// Array methods that don't mutate should still work
101+
expect(frozenArray.map(x => x * 2)).toEqual([2, 4, 6]);
102+
expect(frozenArray.filter(x => x > 1)).toEqual([2, 3]);
103+
});
104+
});

packages/core/tsconfig.build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"./src/js/playground/*.tsx",
88
"./src/js/**/*.web.ts",
99
"./src/js/**/*.web.tsx",
10-
"./typings/react-native.d.ts"
10+
"./typings/react-native.d.ts",
11+
"./typings/global.d.ts"
1112
],
1213
"exclude": ["node_modules"],
1314
"compilerOptions": {

packages/core/typings/global.d.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Global type augmentations for the Sentry React Native SDK
3+
*
4+
* This file contains global type fixes and augmentations to resolve conflicts
5+
* with transitive dependencies.
6+
*/
7+
8+
/**
9+
* Fix for Object.freeze type pollution from @sentry-internal/replay
10+
*
11+
* Issue: TypeScript incorrectly resolves Object.freeze() to a freeze method
12+
* from @sentry-internal/replay's CanvasManagerInterface instead of the built-in.
13+
*
14+
* See: https://github.com/getsentry/sentry-react-native/issues/5407
15+
*/
16+
declare global {
17+
interface ObjectConstructor {
18+
freeze<T>(o: T): Readonly<T>;
19+
20+
// eslint-disable-next-line @typescript-eslint/ban-types -- Matching TypeScript's official Object.freeze signature from lib.es5.d.ts
21+
freeze<T extends Function>(f: T): T;
22+
23+
freeze<T extends {[idx: string]: U | null | undefined | object}, U extends string | bigint | number | boolean | symbol>(o: T): Readonly<T>;
24+
25+
freeze<T>(o: T): Readonly<T>;
26+
}
27+
}
28+
29+
export {};
30+

0 commit comments

Comments
 (0)