Skip to content

Commit 6890765

Browse files
feat: env vars should be readonly (#111)
Co-authored-by: Julius Marminge <julius0216@outlook.com>
1 parent 32222dc commit 6890765

File tree

5 files changed

+98
-39
lines changed

5 files changed

+98
-39
lines changed

.changeset/nervous-houses-cover.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@t3-oss/env-core": patch
3+
---
4+
5+
fix: mark type as readonly

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ dist
99
.vercel
1010
.DS_Store
1111

12+
env.d.ts
1213
next-env.d.ts
1314
**/.vscode

packages/core/index.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,12 @@ export function createEnv<
149149
TShared extends Record<string, ZodType> = NonNullable<unknown>
150150
>(
151151
opts: EnvOptions<TPrefix, TServer, TClient, TShared>
152-
): Simplify<
153-
z.infer<ZodObject<TServer>> &
154-
z.infer<ZodObject<TClient>> &
155-
z.infer<ZodObject<TShared>>
152+
): Readonly<
153+
Simplify<
154+
z.infer<ZodObject<TServer>> &
155+
z.infer<ZodObject<TClient>> &
156+
z.infer<ZodObject<TShared>>
157+
>
156158
> {
157159
const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env;
158160

@@ -209,6 +211,16 @@ export function createEnv<
209211
}
210212
return target[prop as keyof typeof target];
211213
},
214+
// Maybe reconsider this in the future:
215+
// https://github.com/t3-oss/t3-env/pull/111#issuecomment-1682931526
216+
// set(_target, prop) {
217+
// // Readonly - this is the error message you get from assigning to a frozen object
218+
// throw new Error(
219+
// typeof prop === "string"
220+
// ? `Cannot assign to read only property ${prop} of object #<Object>`
221+
// : `Cannot assign to read only property of object #<Object>`
222+
// );
223+
// },
212224
});
213225

214226
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any

packages/core/test/smoke.test.ts

+57-22
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,12 @@ describe("return type is correctly inferred", () => {
106106
},
107107
});
108108

109-
expectTypeOf(env).toEqualTypeOf<{
110-
BAR: string;
111-
FOO_BAR: string;
112-
}>();
109+
expectTypeOf(env).toEqualTypeOf<
110+
Readonly<{
111+
BAR: string;
112+
FOO_BAR: string;
113+
}>
114+
>();
113115

114116
expect(env).toMatchObject({
115117
BAR: "bar",
@@ -128,10 +130,12 @@ describe("return type is correctly inferred", () => {
128130
},
129131
});
130132

131-
expectTypeOf(env).toEqualTypeOf<{
132-
BAR: number;
133-
FOO_BAR: string;
134-
}>();
133+
expectTypeOf(env).toEqualTypeOf<
134+
Readonly<{
135+
BAR: number;
136+
FOO_BAR: string;
137+
}>
138+
>();
135139

136140
expect(env).toMatchObject({
137141
BAR: 123,
@@ -149,9 +153,11 @@ describe("return type is correctly inferred", () => {
149153
},
150154
});
151155

152-
expectTypeOf(env).toEqualTypeOf<{
153-
BAR: string;
154-
}>();
156+
expectTypeOf(env).toEqualTypeOf<
157+
Readonly<{
158+
BAR: string;
159+
}>
160+
>();
155161

156162
expect(env).toMatchObject({
157163
BAR: "bar",
@@ -173,10 +179,12 @@ test("can pass number and booleans", () => {
173179
},
174180
});
175181

176-
expectTypeOf(env).toEqualTypeOf<{
177-
PORT: number;
178-
IS_DEV: boolean;
179-
}>();
182+
expectTypeOf(env).toEqualTypeOf<
183+
Readonly<{
184+
PORT: number;
185+
IS_DEV: boolean;
186+
}>
187+
>();
180188

181189
expect(env).toMatchObject({
182190
PORT: 123,
@@ -280,7 +288,7 @@ describe("client/server only mode", () => {
280288
runtimeEnv: { FOO_BAR: "foo" },
281289
});
282290

283-
expectTypeOf(env).toEqualTypeOf<{ FOO_BAR: string }>();
291+
expectTypeOf(env).toEqualTypeOf<Readonly<{ FOO_BAR: string }>>();
284292
expect(env).toMatchObject({ FOO_BAR: "foo" });
285293
});
286294

@@ -292,7 +300,7 @@ describe("client/server only mode", () => {
292300
runtimeEnv: { BAR: "bar" },
293301
});
294302

295-
expectTypeOf(env).toEqualTypeOf<{ BAR: string }>();
303+
expectTypeOf(env).toEqualTypeOf<Readonly<{ BAR: string }>>();
296304
expect(env).toMatchObject({ BAR: "bar" });
297305
});
298306

@@ -336,11 +344,13 @@ describe("shared can be accessed on both server and client", () => {
336344
runtimeEnv: process.env,
337345
});
338346

339-
expectTypeOf(env).toEqualTypeOf<{
340-
NODE_ENV: "development" | "production" | "test";
341-
BAR: string;
342-
FOO_BAR: string;
343-
}>();
347+
expectTypeOf(env).toEqualTypeOf<
348+
Readonly<{
349+
NODE_ENV: "development" | "production" | "test";
350+
BAR: string;
351+
FOO_BAR: string;
352+
}>
353+
>();
344354

345355
test("server", () => {
346356
const { window } = globalThis;
@@ -369,3 +379,28 @@ describe("shared can be accessed on both server and client", () => {
369379
globalThis.window = window;
370380
});
371381
});
382+
383+
test("envs are readonly", () => {
384+
const env = createEnv({
385+
server: { BAR: z.string() },
386+
runtimeEnv: { BAR: "bar" },
387+
});
388+
389+
/**
390+
* We currently don't enforce readonly during runtime:
391+
* https://github.com/t3-oss/t3-env/pull/111#issuecomment-1682931526
392+
*/
393+
394+
// expect(() => {
395+
// // @ts-expect-error - envs are readonly
396+
// env.BAR = "foo";
397+
// }).toThrowErrorMatchingInlineSnapshot(
398+
// '"Cannot assign to read only property BAR of object #<Object>"'
399+
// );
400+
401+
// expect(env).toMatchObject({ BAR: "bar" });
402+
403+
// @ts-expect-error - envs are readonly
404+
env.BAR = "foo";
405+
expect(env).toMatchObject({ BAR: "foo" });
406+
});

packages/nextjs/test/smoke.test.ts

+19-13
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ test("new experimental runtime option only requires client vars", () => {
114114
},
115115
});
116116

117-
expectTypeOf(env).toEqualTypeOf<{
118-
BAR: string;
119-
NEXT_PUBLIC_BAR: string;
120-
NODE_ENV: "development" | "production";
121-
}>();
117+
expectTypeOf(env).toEqualTypeOf<
118+
Readonly<{
119+
BAR: string;
120+
NEXT_PUBLIC_BAR: string;
121+
NODE_ENV: "development" | "production";
122+
}>
123+
>();
122124

123125
expect(env).toMatchObject({
124126
BAR: "bar",
@@ -138,10 +140,12 @@ describe("return type is correctly inferred", () => {
138140
},
139141
});
140142

141-
expectTypeOf(env).toEqualTypeOf<{
142-
BAR: string;
143-
NEXT_PUBLIC_BAR: string;
144-
}>();
143+
expectTypeOf(env).toEqualTypeOf<
144+
Readonly<{
145+
BAR: string;
146+
NEXT_PUBLIC_BAR: string;
147+
}>
148+
>();
145149

146150
expect(env).toMatchObject({
147151
BAR: "bar",
@@ -159,10 +163,12 @@ describe("return type is correctly inferred", () => {
159163
},
160164
});
161165

162-
expectTypeOf(env).toEqualTypeOf<{
163-
BAR: number;
164-
NEXT_PUBLIC_BAR: string;
165-
}>();
166+
expectTypeOf(env).toEqualTypeOf<
167+
Readonly<{
168+
BAR: number;
169+
NEXT_PUBLIC_BAR: string;
170+
}>
171+
>();
166172

167173
expect(env).toMatchObject({
168174
BAR: 123,

0 commit comments

Comments
 (0)