Skip to content

Commit b5dabe9

Browse files
authored
feat: add support session typing (#13320)
* feat: add support session typing * Add type tests * Fix test * Remove unused import
1 parent 5d8f71c commit b5dabe9

File tree

5 files changed

+121
-2
lines changed

5 files changed

+121
-2
lines changed

.changeset/slimy-bulldogs-sell.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Adds support for typing experimental session data
6+
7+
You can add optional types to your session data by creating a `src/env.d.ts` file in your project that extends the global `App.SessionData` interface. For example:
8+
9+
```ts
10+
declare namespace App {
11+
interface SessionData {
12+
user: {
13+
id: string;
14+
email: string;
15+
};
16+
lastLogin: Date;
17+
}
18+
}
19+
```
20+
21+
Any keys not defined in this interface will be treated as `any`.
22+
23+
Then when you access `Astro.session` in your components, any defined keys will be typed correctly:
24+
25+
```astro
26+
---
27+
const user = await Astro.session.get('user');
28+
// ^? const: user: { id: string; email: string; } | undefined
29+
30+
const something = await Astro.session.get('something');
31+
// ^? const: something: any
32+
33+
Astro.session.set('user', 1);
34+
// ^? Argument of type 'number' is not assignable to parameter of type '{ id: string; email: string; }'.
35+
---
36+
```
37+
38+
See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information.

packages/astro/src/core/session.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
105105
/**
106106
* Gets a session value. Returns `undefined` if the session or value does not exist.
107107
*/
108-
async get<T = any>(key: string): Promise<T | undefined> {
108+
async get<T = void, K extends string = string>(
109+
key: K,
110+
): Promise<
111+
(T extends void ? (K extends keyof App.SessionData ? App.SessionData[K] : any) : T) | undefined
112+
> {
109113
return (await this.#ensureData()).get(key)?.data;
110114
}
111115

@@ -152,7 +156,15 @@ export class AstroSession<TDriver extends SessionDriverName = any> {
152156
* Sets a session value. The session is created if it does not exist.
153157
*/
154158

155-
set<T = any>(key: string, value: T, { ttl }: { ttl?: number } = {}) {
159+
set<T = void, K extends string = string>(
160+
key: K,
161+
value: T extends void
162+
? K extends keyof App.SessionData
163+
? App.SessionData[K]
164+
: any
165+
: NoInfer<T>,
166+
{ ttl }: { ttl?: number } = {},
167+
) {
156168
if (!key) {
157169
throw new AstroError({
158170
...SessionStorageSaveError,

packages/astro/src/types/public/extendables.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ declare global {
1010
* Used by middlewares to store information, that can be read by the user via the global `Astro.locals`
1111
*/
1212
export interface Locals {}
13+
14+
/**
15+
* Optionally type the data stored in the session
16+
*/
17+
export interface SessionData {}
1318
}
1419

1520
namespace Astro {
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import "./session-env";
2+
import { describe, it } from 'node:test';
3+
import { expectTypeOf } from 'expect-type';
4+
import type { AstroCookies, ResolvedSessionConfig } from '../../dist/types/public/index.js';
5+
import { AstroSession } from '../../dist/core/session.js';
6+
7+
const defaultMockCookies = {
8+
set: () => {},
9+
delete: () => {},
10+
get: () => 'astro cookie',
11+
};
12+
13+
14+
const defaultConfig: ResolvedSessionConfig<'memory'> = {
15+
driver: 'memory',
16+
cookie: 'test-session',
17+
ttl: 60,
18+
options: {},
19+
};
20+
21+
// Helper to create a new session instance with mocked dependencies
22+
function createSession() {
23+
return new AstroSession(defaultMockCookies as unknown as AstroCookies, defaultConfig);
24+
}
25+
26+
describe('Session', () => {
27+
it('Types session.get return values', () => {
28+
const session = createSession();
29+
30+
expectTypeOf(session.get('value')).resolves.toEqualTypeOf<string | undefined>();
31+
32+
expectTypeOf(session.get('cart')).resolves.toEqualTypeOf<Array<string> | undefined>();
33+
34+
expectTypeOf(session.get('unknown')).resolves.toEqualTypeOf<any>();
35+
36+
});
37+
38+
it('Types session.set arguments', () => {
39+
const session = createSession();
40+
41+
expectTypeOf(session.set('value', 'test')).toEqualTypeOf<void>();
42+
expectTypeOf(session.set('cart', ['item1', 'item2'])).toEqualTypeOf<void>();
43+
expectTypeOf(session.set('unknown', {})).toEqualTypeOf<void>();
44+
45+
// Testing invalid types
46+
// @ts-expect-error This should fail because the value is not a string
47+
expectTypeOf(session.set('value', 1)).toEqualTypeOf<void>();
48+
// @ts-expect-error This should fail because the value is not an array
49+
expectTypeOf(session.set('cart', 'invalid')).toEqualTypeOf<void>();
50+
});
51+
52+
});
53+
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare namespace App {
2+
interface SessionData {
3+
value: string;
4+
cart: Array<string>;
5+
key: { value: 'none' | 'expected' | 'unexpected' };
6+
user: {
7+
id: string;
8+
email: string;
9+
};
10+
}
11+
}

0 commit comments

Comments
 (0)