Bläddra i källkod

Make sure type checking is working with dates and strings

arvid-e 9 månader sedan
förälder
incheckning
d1a9e98d7c
2 ändrade filer med 39 tillägg och 16 borttagningar
  1. 13 12
      apps/app/src/utils/axios-date-conversion.spec.ts
  2. 26 4
      apps/app/src/utils/axios.ts

+ 13 - 12
apps/app/src/utils/axios-date-conversion.spec.ts

@@ -1,4 +1,5 @@
 import { convertStringsToDates } from './axios';
 import { convertStringsToDates } from './axios';
+import type { IsoDateString } from './axios';
 
 
 
 
 describe('convertStringsToDates', () => {
 describe('convertStringsToDates', () => {
@@ -8,7 +9,7 @@ describe('convertStringsToDates', () => {
     const dateString = '2023-01-15T10:00:00.000Z';
     const dateString = '2023-01-15T10:00:00.000Z';
     const input = {
     const input = {
       id: 1,
       id: 1,
-      createdAt: dateString,
+      createdAt: dateString as IsoDateString,
       name: 'Test Item',
       name: 'Test Item',
     };
     };
     const expected = {
     const expected = {
@@ -29,12 +30,12 @@ describe('convertStringsToDates', () => {
     const input = {
     const input = {
       data: {
       data: {
         item1: {
         item1: {
-          updatedAt: dateString1,
+          updatedAt: dateString1 as IsoDateString,
           value: 10,
           value: 10,
         },
         },
         item2: {
         item2: {
           nested: {
           nested: {
-            deletedAt: dateString2,
+            deletedAt: dateString2 as IsoDateString,
             isActive: false,
             isActive: false,
           },
           },
         },
         },
@@ -67,8 +68,8 @@ describe('convertStringsToDates', () => {
     const dateString1 = '2023-04-05T14:15:00.000Z';
     const dateString1 = '2023-04-05T14:15:00.000Z';
     const dateString2 = '2023-05-10T16:00:00.000Z';
     const dateString2 = '2023-05-10T16:00:00.000Z';
     const input = [
     const input = [
-      { id: 1, eventDate: dateString1 },
-      { id: 2, eventDate: dateString2, data: { nestedProp: 'value' } },
+      { id: 1, eventDate: dateString1 as IsoDateString },
+      { id: 2, eventDate: dateString2 as IsoDateString, data: { nestedProp: 'value' } },
     ];
     ];
     const expected = [
     const expected = [
       { id: 1, eventDate: new Date(dateString1) },
       { id: 1, eventDate: new Date(dateString1) },
@@ -85,7 +86,7 @@ describe('convertStringsToDates', () => {
   // Test case 4: Array containing date strings directly (though less common for this function)
   // Test case 4: Array containing date strings directly (though less common for this function)
   test('should handle arrays containing date strings directly', () => {
   test('should handle arrays containing date strings directly', () => {
     const dateString = '2023-06-20T18:00:00.000Z';
     const dateString = '2023-06-20T18:00:00.000Z';
-    const input = ['text', dateString, 123];
+    const input: [string, IsoDateString, number] = ['text', dateString as IsoDateString, 123];
     const expected = ['text', new Date(dateString), 123];
     const expected = ['text', new Date(dateString), 123];
     const result = convertStringsToDates(input);
     const result = convertStringsToDates(input);
     expect(result[1]).toBeInstanceOf(Date);
     expect(result[1]).toBeInstanceOf(Date);
@@ -129,7 +130,7 @@ describe('convertStringsToDates', () => {
   // Test case 8: Date string with different milliseconds (isoDateRegex without .000)
   // Test case 8: Date string with different milliseconds (isoDateRegex without .000)
   test('should handle date strings with varied milliseconds', () => {
   test('should handle date strings with varied milliseconds', () => {
     const dateString = '2023-01-15T10:00:00Z'; // No milliseconds
     const dateString = '2023-01-15T10:00:00Z'; // No milliseconds
-    const input = { createdAt: dateString };
+    const input = { createdAt: dateString as IsoDateString };
     const expected = { createdAt: new Date(dateString) };
     const expected = { createdAt: new Date(dateString) };
     const result = convertStringsToDates(input);
     const result = convertStringsToDates(input);
     expect(result.createdAt).toBeInstanceOf(Date);
     expect(result.createdAt).toBeInstanceOf(Date);
@@ -167,9 +168,9 @@ describe('convertStringsToDates', () => {
     const dateStringWithOffset = '2025-06-12T14:00:00+09:00';
     const dateStringWithOffset = '2025-06-12T14:00:00+09:00';
     const input = {
     const input = {
       id: 2,
       id: 2,
-      eventTime: dateStringWithOffset,
+      eventTime: dateStringWithOffset as IsoDateString,
       details: {
       details: {
-        lastActivity: '2025-06-12T05:00:00-04:00',
+        lastActivity: '2025-06-12T05:00:00-04:00' as IsoDateString,
       },
       },
     };
     };
     const expected = {
     const expected = {
@@ -194,7 +195,7 @@ describe('convertStringsToDates', () => {
   test('should convert ISO date strings with negative UTC offset (-05:00) to Date objects', () => {
   test('should convert ISO date strings with negative UTC offset (-05:00) to Date objects', () => {
     const dateStringWithNegativeOffset = '2025-01-01T10:00:00-05:00';
     const dateStringWithNegativeOffset = '2025-01-01T10:00:00-05:00';
     const input = {
     const input = {
-      startTime: dateStringWithNegativeOffset,
+      startTime: dateStringWithNegativeOffset as IsoDateString,
     };
     };
     const expected = {
     const expected = {
       startTime: new Date(dateStringWithNegativeOffset),
       startTime: new Date(dateStringWithNegativeOffset),
@@ -211,7 +212,7 @@ describe('convertStringsToDates', () => {
   test('should convert ISO date strings with explicit zero UTC offset (+00:00) to Date objects', () => {
   test('should convert ISO date strings with explicit zero UTC offset (+00:00) to Date objects', () => {
     const dateStringWithZeroOffset = '2025-03-15T12:00:00+00:00';
     const dateStringWithZeroOffset = '2025-03-15T12:00:00+00:00';
     const input = {
     const input = {
-      zeroOffsetDate: dateStringWithZeroOffset,
+      zeroOffsetDate: dateStringWithZeroOffset as IsoDateString,
     };
     };
     const expected = {
     const expected = {
       zeroOffsetDate: new Date(dateStringWithZeroOffset),
       zeroOffsetDate: new Date(dateStringWithZeroOffset),
@@ -228,7 +229,7 @@ describe('convertStringsToDates', () => {
   test('should convert ISO date strings with milliseconds and UTC offset to Date objects', () => {
   test('should convert ISO date strings with milliseconds and UTC offset to Date objects', () => {
     const dateStringWithMsAndOffset = '2025-10-20T23:59:59.999-07:00';
     const dateStringWithMsAndOffset = '2025-10-20T23:59:59.999-07:00';
     const input = {
     const input = {
-      detailedTime: dateStringWithMsAndOffset,
+      detailedTime: dateStringWithMsAndOffset as IsoDateString,
     };
     };
     const expected = {
     const expected = {
       detailedTime: new Date(dateStringWithMsAndOffset),
       detailedTime: new Date(dateStringWithMsAndOffset),

+ 26 - 4
apps/app/src/utils/axios.ts

@@ -8,15 +8,37 @@ export * from 'axios';
 
 
 const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(Z|[+-]\d{2}:\d{2})$/;
 const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(Z|[+-]\d{2}:\d{2})$/;
 
 
+// For type checking for Date and String types to work
+export type IsoDateString = string & { readonly __isIsoDateString: unique symbol };
+
+type IsTuple<T> = T extends readonly any[]
+    ? number extends T['length']
+        ? false // Not a tuple (it's a regular array)
+        : true // It's a tuple
+    : false;
+
 // Utility type to decide the resulting type of every input object at compile time.
 // Utility type to decide the resulting type of every input object at compile time.
-type DeepDateConvert<T> = T extends string
-    ? (string extends T ? T : (RegExpMatchArray extends T ? T : Date)) // If T is exactly string it becomes Date
+type DeepDateConvert<T> = T extends Date
+    ? T
+    : T extends IsoDateString
+    ? Date
+    : T extends string
+    ? T
+    : IsTuple<T> extends true
+    ? { [K in keyof T]: DeepDateConvert<T[K]> }
     : T extends (infer U)[]
     : T extends (infer U)[]
-    ? DeepDateConvert<U>[] // If T is an array, map its elements
+    ? DeepDateConvert<U>[]
     : T extends object
     : T extends object
-    ? { [K in keyof T]: DeepDateConvert<T[K]> } // If T is an object, map its properties
+    ? { [K in keyof T]: DeepDateConvert<T[K]> }
     : T;
     : T;
 
 
+/**
+* Converts string to dates recursively.
+*
+* @param data - Data to be transformed to Date if applicable.
+* @param seen - Set containing data that has been through the function before.
+* @returns - Data containing transformed Dates.
+*/
 function convertStringsToDatesRecursive<T>(data: T, seen: Set<any>): DeepDateConvert<T> {
 function convertStringsToDatesRecursive<T>(data: T, seen: Set<any>): DeepDateConvert<T> {
   if (typeof data !== 'object' || data === null) {
   if (typeof data !== 'object' || data === null) {
     if (typeof data === 'string' && isoDateRegex.test(data)) {
     if (typeof data === 'string' && isoDateRegex.test(data)) {