Валидация на границе системы предотвращает ошибки, некорректный вывод и потенциальные проблемы безопасности. Особенно важна при работе с данными из API, пользовательского ввода или localStorage.
Date, string,
number).NaN, не
Infinity).null, не
undefined).type DateInput = Date | string | number;
type ValidationResult = { valid: true; date: Date } | { valid: false; reason: string };
function validateDateInput(input: unknown): ValidationResult {
if (input === null || input === undefined) {
return { valid: false, reason: 'Value is null or undefined' };
}
if (typeof input !== 'string' && typeof input !== 'number' && !(input instanceof Date)) {
return { valid: false, reason: `Unexpected type: ${typeof input}` };
}
if (typeof input === 'number' && !isFinite(input)) {
return { valid: false, reason: `Non-finite number: ${input}` };
}
const d = new Date(input as any);
if (isNaN(d.getTime())) {
return { valid: false, reason: `Cannot parse date: "${input}"` };
}
return { valid: true, date: d };
}
const MIN_DATE = new Date('2000-01-01T00:00:00Z').getTime();
const MAX_DATE = () => Date.now() + 365 * 86400_000; // Не более года в будущем
function validateDateRange(d: Date): string | null {
const ts = d.getTime();
if (ts < MIN_DATE) {
return `Date is too far in the past: ${d.toISOString()}`;
}
if (ts > MAX_DATE()) {
return `Date is too far in the future: ${d.toISOString()}`;
}
return null; // Ошибок нет
}
import { format } from 'timeago.js';
function safeFormat(input: unknown, locale = 'ru'): string {
const validation = validateDateInput(input);
if (!validation.valid) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[timeago] Invalid date input: ${validation.reason}`);
}
return '';
}
const rangeError = validateDateRange(validation.date);
if (rangeError) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[timeago] Date out of range: ${rangeError}`);
}
}
return format(validation.date, locale);
}
const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+\-]\d{2}:\d{2})?)?$/;
function isISO8601(str: string): boolean {
return ISO_8601_REGEX.test(str);
}
function validateISOString(input: unknown): ValidationResult {
if (typeof input !== 'string') {
return { valid: false, reason: 'Not a string' };
}
if (!isISO8601(input)) {
return { valid: false, reason: `Not ISO 8601 format: "${input}"` };
}
const d = new Date(input);
if (isNaN(d.getTime())) {
return { valid: false, reason: `ISO string parses to invalid date: "${input}"` };
}
return { valid: true, date: d };
}
function validateTimestamp(ts: unknown): ValidationResult {
if (typeof ts !== 'number') {
return { valid: false, reason: `Not a number: ${typeof ts}` };
}
if (!isFinite(ts)) {
return { valid: false, reason: `Non-finite value: ${ts}` };
}
// Timestamp в секундах (Unix)? Конвертировать
const adjustedTs = ts < 1e10 ? ts * 1000 : ts;
const d = new Date(adjustedTs);
if (isNaN(d.getTime())) {
return { valid: false, reason: `Invalid timestamp: ${ts}` };
}
return { valid: true, date: d };
}
import { z } from 'zod';
const PostSchema = z.object({
id: z.number(),
title: z.string(),
createdAt: z.string().datetime(), // ISO 8601 с Z
});
type Post = z.infer<typeof PostSchema>;
function formatPostDate(post: Post, locale = 'ru'): string {
// post.createdAt уже валидирован Zod — гарантированно ISO строка
return format(post.createdAt, locale);
}
async function fetchPosts(): Promise<Post[]> {
const response = await fetch('/api/posts');
const data = await response.json();
// Валидировать перед использованием
return data.map((item: unknown) => {
const result = PostSchema.safeParse(item);
if (!result.success) {
console.error('Invalid post data:', result.error);
return null;
}
return result.data;
}).filter(Boolean);
}
import { format } from 'timeago.js';
interface TimeAgoProps {
date?: Date | string | number;
locale?: string;
fallback?: string;
}
function TimeAgo({ date, locale = 'ru', fallback = '' }: TimeAgoProps) {
if (!date) return <>{fallback}</>;
const d = new Date(date as any);
if (isNaN(d.getTime())) return <>{fallback}</>;
return <time dateTime={d.toISOString()}>{format(d, locale)}</time>;
}
// PropTypes для JavaScript проектов
TimeAgo.defaultProps = { locale: 'ru', fallback: '' };
| Источник | Тип данных | Риск | Решение |
|---|---|---|---|
| Бэкенд API (доверенный) | ISO строка | Низкий | validateDateInput |
| URL параметр | Строка | Высокий | validateISO + range check |
| localStorage | Строка | Средний | validateDateInput |
| Пользовательский ввод | Произвольная строка | Высокий | Полная валидация |
| Внутренние данные | Известный тип | Минимальный | Тип в TypeScript |