Introduction
In modern Node.js environments, including NestJS, security best practices demand that configuration values such as JWT expiration times are managed dynamically via environment variables. However, this practice often leads to a stubborn TypeScript compilation failure known as TS2769, which signifies a fundamental conflict between TypeScript's static type inference and the specific type requirements of the underlying jsonwebtoken library.
This guide provides multiple professional solutions to this common problem, with emphasis on both type safety and runtime validation.
1. The Scenario: Dynamic Expiry Configuration
The issue arises when attempting to pass a string retrieved from a configuration service directly into the expiresIn option during JWT token signing.
1.1 Environment Setup
We define clear, human-readable expiration times in our environment file:
# .env file configuration
JWT_SECRET=YOUR_HARD_TO_GUESS_SECRET_KEY
# Token expiration times must be strings compatible with the 'ms' library
# Valid formats: '15m', '2h', '7d', '1000' (milliseconds as string)
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d
1.2 The Failing Implementation
The most logical approach—injecting the ConfigService and retrieving values as strings—fails compilation:
// src/auth/auth.service.ts (The Code That Fails Compilation)
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
getAccessToken(payload: any) {
// 🛑 ERROR: TypeScript throws TS2769 here
const accessToken = this.jwtService.sign(payload, {
expiresIn: this.configService.get('ACCESS_TOKEN_EXPIRY'),
});
return accessToken;
}
}
2. The Diagnosis: Understanding TS2769
The error message states: "Type 'string' is not assignable to type 'number | string | undefined'" or more specifically references the StringValue type.
Why This Happens
-
Generic String Type: Methods like
ConfigService.get()return a generic JavaScriptstringtype -
Specialized Union Type: The
jsonwebtokenlibrary (used internally by@nestjs/jwt) requiresexpiresInto be either:- A
number(representing seconds) - A
StringValuetype (a specialized string type matching themslibrary format like '1h', '30s', '7d') undefined
- A
Type System Limitation: TypeScript cannot statically guarantee that your dynamic string matches the
mslibrary format, so it conservatively prevents compilation
Understanding StringValue
The StringValue type represents strings that the ms library can parse:
-
Valid examples:
'2 days','1d','10h','2.5h','30s','1000'(ms as string) -
Invalid examples:
'tomorrow','next week','invalid'
3. Professional Solutions
Solution 1: Type Assertion (Quick Fix)
The fastest solution uses explicit type assertion to bridge the type gap:
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { SignOptions } from 'jsonwebtoken'; // ⭐ Import the type
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
getTokens(userId: string, email: string) {
const payload = { sub: userId, email };
// Retrieve and assert the type
const accessExpiry = (
this.configService.get('ACCESS_TOKEN_EXPIRY') || '15m'
) as SignOptions['expiresIn']; // 🚀 Type assertion
const accessToken = this.jwtService.sign(payload, {
expiresIn: accessExpiry,
});
return { accessToken };
}
}
Pros: Simple, minimal code changes
Cons: Bypasses type safety, no runtime validation
Solution 2: Runtime Validation (Recommended for Production)
A more robust approach includes runtime validation to ensure the environment variable is valid:
// src/auth/auth.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { SignOptions } from 'jsonwebtoken';
import ms from 'ms';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
private validateExpiry(expiry: string): string {
try {
// This will throw if the format is invalid
ms(expiry);
return expiry;
} catch (error) {
throw new InternalServerErrorException(
`Invalid JWT expiry format: ${expiry}. Use formats like '15m', '7d', '2h'`
);
}
}
getTokens(userId: string, email: string) {
const payload = { sub: userId, email };
const accessExpiryRaw = this.configService.get('ACCESS_TOKEN_EXPIRY') || '15m';
const refreshExpiryRaw = this.configService.get('REFRESH_TOKEN_EXPIRY') || '7d';
// Validate before using
const accessExpiry = this.validateExpiry(accessExpiryRaw) as SignOptions['expiresIn'];
const refreshExpiry = this.validateExpiry(refreshExpiryRaw) as SignOptions['expiresIn'];
const accessToken = this.jwtService.sign(payload, {
expiresIn: accessExpiry,
});
const refreshToken = this.jwtService.sign(payload, {
expiresIn: refreshExpiry,
});
return { accessToken, refreshToken };
}
}
Pros: Catches configuration errors early, fails safely
Cons: Slightly more code
Solution 3: Centralized Configuration with Type Safety
For larger applications, create a dedicated configuration service:
// src/config/jwt.config.ts
import { registerAs } from '@nestjs/config';
import { SignOptions } from 'jsonwebtoken';
import ms from 'ms';
function validateExpiry(expiry: string): string {
try {
ms(expiry);
return expiry;
} catch {
throw new Error(`Invalid JWT expiry: ${expiry}`);
}
}
export default registerAs('jwt', () => {
const accessExpiry = process.env.ACCESS_TOKEN_EXPIRY || '15m';
const refreshExpiry = process.env.REFRESH_TOKEN_EXPIRY || '7d';
return {
secret: process.env.JWT_SECRET,
accessExpiry: validateExpiry(accessExpiry) as SignOptions['expiresIn'],
refreshExpiry: validateExpiry(refreshExpiry) as SignOptions['expiresIn'],
};
});
// Usage in service
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
@Inject(jwtConfig.KEY) private jwt: ConfigType<typeof jwtConfig>,
) {}
getTokens(userId: string, email: string) {
const payload = { sub: userId, email };
const accessToken = this.jwtService.sign(payload, {
expiresIn: this.jwt.accessExpiry, // ✅ Type-safe and validated
});
return { accessToken };
}
}
Pros: Centralized validation, fails at startup (not runtime), excellent type safety
Cons: More initial setup
4. Alternative Import Paths
In NestJS projects, you can import SignOptions from multiple locations:
// Option 1: Direct from jsonwebtoken (most common)
import { SignOptions } from 'jsonwebtoken';
// Option 2: From @nestjs/jwt (if re-exported in your version)
import { JwtSignOptions } from '@nestjs/jwt';
Both approaches work, but importing from jsonwebtoken directly is more explicit and version-agnostic.
5. Testing Your Configuration
Always validate your JWT configuration during application bootstrap:
// src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Test JWT configuration on startup
const configService = app.get(ConfigService);
const accessExpiry = configService.get('ACCESS_TOKEN_EXPIRY');
console.log(`✅ JWT Access Token Expiry: ${accessExpiry}`);
await app.listen(3000);
}
Conclusion
The TS2769 error when configuring JWT expiration times is a common challenge when integrating dynamic configuration with strict TypeScript type definitions. The solutions presented here range from quick type assertions to comprehensive runtime validation:
- Use Solution 1 for rapid prototyping or simple applications
- Use Solution 2 for production applications requiring runtime safety
- Use Solution 3 for enterprise applications with complex configuration needs
All approaches use type assertion (as SignOptions['expiresIn']) to satisfy TypeScript's compiler while maintaining the flexibility of environment-based configuration. The key difference lies in when and how you validate the configuration values.
Key Takeaways
- Type assertions bypass compile-time safety for runtime flexibility
- Always validate environment variables that affect security (like JWT expiration)
- Fail early (at startup) rather than during request handling
- Use the
mslibrary's validation to ensure format correctness - Document valid formats in your
.env.examplefiles
By combining type assertions with proper runtime validation, you maintain both TypeScript's type safety benefits and the flexibility required for production deployments.
Top comments (0)