Skip to content

Commit 149d547

Browse files
fix: prevent crash when date props are passed as strings
Added safeToDate() helper function that validates date values at runtime, returning null for invalid inputs (strings, numbers, invalid Date objects). This allows graceful fallback instead of crashing with "getTime/getFullYear is not a function" errors. Fixed locations: - time.tsx: isSelectedTime(), renderTimes() - validate selected/openToDate - index.tsx: handleTimeOnlyArrowKey(), handleTimeOnlyInputKeyDown() - validate selected - index.tsx: handleInputChange() - validate startDate/endDate before .getTime() Added tests: - 11 unit tests for safeToDate() function - 5 integration tests for string date prop handling in TimePicker Fixes #5964 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent be38873 commit 149d547

5 files changed

Lines changed: 188 additions & 7 deletions

File tree

src/date_utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,24 @@ export function isValid(date: Date): boolean {
368368
return isValidDate(date);
369369
}
370370

371+
/**
372+
* Safely returns a valid Date or null.
373+
* This handles cases where a value might be passed as a string or other
374+
* invalid type at runtime, even though TypeScript expects a Date.
375+
* @param date - The value to check (typed as Date but could be anything at runtime)
376+
* @returns The date if it's a valid Date object, otherwise null
377+
*/
378+
export function safeToDate(date: Date | null | undefined): Date | null {
379+
if (date == null) {
380+
return null;
381+
}
382+
// Check if it's actually a Date object AND is valid
383+
if (isDate(date) && isValidDate(date)) {
384+
return date;
385+
}
386+
return null;
387+
}
388+
371389
// ** Date Formatting **
372390

373391
/**

src/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
isSameMinute,
5353
toZonedTime,
5454
fromZonedTime,
55+
safeToDate,
5556
type HighlightDate,
5657
type HolidayItem,
5758
type TimeZone,
@@ -783,8 +784,10 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
783784
strictParsing,
784785
)
785786
: null;
786-
const startChanged = startDate?.getTime() !== startDateNew?.getTime();
787-
const endChanged = endDate?.getTime() !== endDateNew?.getTime();
787+
const startChanged =
788+
safeToDate(startDate)?.getTime() !== startDateNew?.getTime();
789+
const endChanged =
790+
safeToDate(endDate)?.getTime() !== endDateNew?.getTime();
788791

789792
if (!startChanged && !endChanged) {
790793
return;
@@ -1231,7 +1234,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
12311234

12321235
handleTimeOnlyArrowKey = (eventKey: string): void => {
12331236
const currentTime =
1234-
this.props.selected || this.state.preSelection || newDate();
1237+
safeToDate(this.props.selected) || this.state.preSelection || newDate();
12351238
const timeIntervals = this.props.timeIntervals ?? 30;
12361239
const dateFormat =
12371240
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
@@ -1293,7 +1296,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
12931296
const timeFormat = this.props.timeFormat || "p";
12941297

12951298
const defaultTime =
1296-
this.state.preSelection || this.props.selected || newDate();
1299+
this.state.preSelection || safeToDate(this.props.selected) || newDate();
12971300
const parsedDate = parseDate(
12981301
inputValue,
12991302
dateFormat,

src/test/date_utils_test.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
registerLocale,
5757
isMonthYearDisabled,
5858
getDefaultLocale,
59+
safeToDate,
5960
} from "../date_utils";
6061

6162
registerLocale("pt-BR", ptBR);
@@ -1733,4 +1734,67 @@ describe("date_utils", () => {
17331734
expect(typeof result).toBe("boolean");
17341735
});
17351736
});
1737+
1738+
describe("safeToDate", () => {
1739+
it("returns the date when given a valid Date object", () => {
1740+
const date = new Date("2024-01-15");
1741+
const result = safeToDate(date);
1742+
expect(result).toBe(date);
1743+
});
1744+
1745+
it("returns null when given null", () => {
1746+
const result = safeToDate(null);
1747+
expect(result).toBeNull();
1748+
});
1749+
1750+
it("returns null when given undefined", () => {
1751+
const result = safeToDate(undefined);
1752+
expect(result).toBeNull();
1753+
});
1754+
1755+
it("returns null when given a string", () => {
1756+
// TypeScript types this as Date, but at runtime it could be a string
1757+
const result = safeToDate("2024-01-15" as unknown as Date);
1758+
expect(result).toBeNull();
1759+
});
1760+
1761+
it("returns null when given an invalid date string", () => {
1762+
const result = safeToDate("not-a-date" as unknown as Date);
1763+
expect(result).toBeNull();
1764+
});
1765+
1766+
it("returns null when given an Invalid Date object", () => {
1767+
const invalidDate = new Date("invalid");
1768+
expect(isValid(invalidDate)).toBe(false);
1769+
const result = safeToDate(invalidDate);
1770+
expect(result).toBeNull();
1771+
});
1772+
1773+
it("returns null when given a number", () => {
1774+
const result = safeToDate(1705276800000 as unknown as Date);
1775+
expect(result).toBeNull();
1776+
});
1777+
1778+
it("returns null when given an object that is not a Date", () => {
1779+
const result = safeToDate({ year: 2024, month: 1 } as unknown as Date);
1780+
expect(result).toBeNull();
1781+
});
1782+
1783+
it("returns the date when given a Date created from newDate()", () => {
1784+
const date = newDate();
1785+
const result = safeToDate(date);
1786+
expect(result).toBe(date);
1787+
});
1788+
1789+
it("returns the date when given a Date at epoch", () => {
1790+
const date = new Date(0);
1791+
const result = safeToDate(date);
1792+
expect(result).toBe(date);
1793+
});
1794+
1795+
it("returns null when given an empty string", () => {
1796+
const result = safeToDate("" as unknown as Date);
1797+
expect(result).toBeNull();
1798+
});
1799+
});
17361800
});

src/test/timepicker_test.test.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,4 +1206,95 @@ describe("TimePicker", () => {
12061206
expect(instance.state.open).toBe(true);
12071207
});
12081208
});
1209+
1210+
describe("handling invalid date prop types", () => {
1211+
it("should not crash when selected prop is a string instead of Date", () => {
1212+
// This tests the fix for issue #5964
1213+
// Some users may pass a string to selected prop at runtime
1214+
expect(() => {
1215+
render(
1216+
<TestDatePicker
1217+
inline
1218+
selected={"2024-01-15"}
1219+
showTimeSelect
1220+
timeIntervals={15}
1221+
/>,
1222+
);
1223+
}).not.toThrow();
1224+
});
1225+
1226+
it("should render time options when selected prop is a string", () => {
1227+
const { container } = render(
1228+
<TestDatePicker
1229+
inline
1230+
selected={"2024-01-15"}
1231+
showTimeSelect
1232+
timeIntervals={60}
1233+
/>,
1234+
);
1235+
1236+
const timeList = container.querySelector(".react-datepicker__time-list");
1237+
expect(timeList).not.toBeNull();
1238+
1239+
const timeItems = container.querySelectorAll(
1240+
".react-datepicker__time-list-item",
1241+
);
1242+
expect(timeItems.length).toBeGreaterThan(0);
1243+
});
1244+
1245+
it("should not crash when openToDate prop is a string instead of Date", () => {
1246+
expect(() => {
1247+
render(
1248+
<TestDatePicker
1249+
inline
1250+
openToDate={"2024-06-15"}
1251+
showTimeSelect
1252+
timeIntervals={15}
1253+
/>,
1254+
);
1255+
}).not.toThrow();
1256+
});
1257+
1258+
it("should fall back to current date when selected is an invalid string", () => {
1259+
const { container } = render(
1260+
<TestDatePicker
1261+
inline
1262+
selected={"not-a-valid-date"}
1263+
showTimeSelect
1264+
timeIntervals={60}
1265+
/>,
1266+
);
1267+
1268+
// Should still render time options (falling back to newDate())
1269+
const timeItems = container.querySelectorAll(
1270+
".react-datepicker__time-list-item",
1271+
);
1272+
expect(timeItems.length).toBeGreaterThan(0);
1273+
});
1274+
1275+
it("should allow selecting a time when selected was initially a string", () => {
1276+
let selectedDate: Date | null = null;
1277+
const handleChange = (date: Date | null) => {
1278+
selectedDate = date;
1279+
};
1280+
1281+
const { container } = render(
1282+
<TestDatePicker
1283+
inline
1284+
selected={"2024-01-15"}
1285+
onChange={handleChange}
1286+
showTimeSelect
1287+
timeIntervals={60}
1288+
/>,
1289+
);
1290+
1291+
const firstTimeItem = container.querySelector(
1292+
".react-datepicker__time-list-item",
1293+
);
1294+
expect(firstTimeItem).not.toBeNull();
1295+
1296+
fireEvent.click(firstTimeItem!);
1297+
expect(selectedDate).toBeInstanceOf(Date);
1298+
});
1299+
});
12091300
});

src/time.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getHoursInDay,
1414
isSameMinute,
1515
getSeconds,
16+
safeToDate,
1617
type Locale,
1718
type TimeFilterOptions,
1819
KeyType,
@@ -139,8 +140,10 @@ export default class Time extends Component<TimeProps, TimeState> {
139140
this.props.onChange?.(time);
140141
};
141142

142-
isSelectedTime = (time: Date) =>
143-
this.props.selected && isSameMinute(this.props.selected, time);
143+
isSelectedTime = (time: Date) => {
144+
const selected = safeToDate(this.props.selected);
145+
return selected && isSameMinute(selected, time);
146+
};
144147

145148
isDisabledTime = (time: Date): boolean | undefined =>
146149
((this.props.minTime || this.props.maxTime) &&
@@ -218,7 +221,9 @@ export default class Time extends Component<TimeProps, TimeState> {
218221
const intervals = this.props.intervals ?? Time.defaultProps.intervals;
219222

220223
const activeDate =
221-
this.props.selected || this.props.openToDate || newDate();
224+
safeToDate(this.props.selected) ||
225+
safeToDate(this.props.openToDate) ||
226+
newDate();
222227

223228
const base = getStartOfDay(activeDate);
224229
const sortedInjectTimes =

0 commit comments

Comments
 (0)