Home > Software engineering >  How to filter a TypeScript array based on maybe-undefined date-as-string item object property?
How to filter a TypeScript array based on maybe-undefined date-as-string item object property?

Time:11-25

The API returns the result as an object with more than thousand values in the following format:

result = {
  "items": [
            {
              "Name": "abc",
              "CreatedOn": "2017-08-29T15:21:20.000Z",
              "Description": "xyz"
            },
            {
              "Name": "def",
              "CreatedOn": "2021-05-21T15:20:20.000Z",
              "Description": "cvb"
            }
          ]
}

I would like to filter items in the object which are more than 90 days old without having to iterate all the items using for loop. In other words I would like to do something like this below but this doesn't work.

var currentDate = new Date();
var newResult = result.items?.filter(function(i) {
    return ((currentDate - Date.parse(i.CreatedOn)) > 90);
}

According to the IDE property CreatedOn is of type string | undefined so it throws the error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.

CodePudding user response:

Somewhere in your project you'll have something like this:

interface Result {
    readonly items: readonly ResultItem[] | null;
}

interface ResultItem {
    readonly Name       : string;
    readonly CreatedOn  : string | undefined;
    readonly Description: string;
}

or this (or variations thereof):

type Result = {
    items?: ResultItem[];
}

interface ResultItem {
    Name       : string;
    CreatedOn? : string;
    Description: string;
}

Or it may be a type instead of an interface (just make sure you never use class to describe JSON data, as JSON object data cannot be a class instance because the constructor never runs).

Also, you should be using camelCase, not PascalCase, for member properties. So use names like createdOn instead of CreatedOn in your generated JSON.

Fortunately You don't need to change the types/interfaces, just change your TypeScript to safely check .CreatedOn and that Date.parse did not return NaN. Like so:

  • The result.items ?? [] part is because your post implies result.items is nullable or maybe-undefined.
  • Note when using map with an =>-style function that you may need to wrap object-literals in () so the JS engine doesn't interpret the { and } as block delimiters.
const result: Result = ...

const currentDate = new Date();

const newResult = (result.items ?? []).filter( e => {
    if( typeof e.CreatedOn === 'string' ) {
        const parsed = Date.parse( e.CreatedOn );
        if( !isNaN( parsed ) ) {
            return ( currentDate - parsed ) > 90;
        }
    }
    return false;
} );

Though personally I'd do it with an initial filter and map steps:

const items       = result.items ?? [];
const currentDate = new Date();

const newResult = items
    .filter( e => typeof e.CreatedOn === 'string' )
    .map( e => ( { ...e, CreatedOn2: Date.parse( e.CreatedOn ) } ) )
    .filter( e => !isNaN( e.CreatedOn2 ) )
    .filter( e => ( currentDate - e.CreatedOn2 ) > 90 ) );

or simplified further:

const items       = result.items ?? [];
const currentDate = new Date();

const newResult = items
    .filter( e => typeof e.CreatedOn === 'string' )
    .map( e => Object.assign( e, { createdOn2: Date.parse( e.CreatedOn ) )
    .filter( e => !isNaN( e.CreatedOn2 ) && ( currentDate - e.CreatedOn2 ) > 90 );

An even better solution:

  • If you're in control of how the JSON is generated then you can ensure that certain (or all) item properties will always be set (and so never undefined or null), so if you can guarantee that all 3 properties are always set (never null or undefined) then update your types/interfaces to this:

    interface ResultItem {
        readonly name       : string;
        readonly createdOn  : string;
        readonly description: string;
    }
    
    • Note the camelCase properties.
    • Immutability of data is a huge benefit, so make sure your interface properties are all readonly, all arrays are readonly T[], and that properties are only annotated with ? or | null or | undefined as-appropriate instead of simply assuming one way or another.
  • So make sure you use strictNullChecks in your tsconfig.json or tsc options! - actually, just use strict always!

  • Also consider changing your JSON DTO from using a string representation of a Date (are there any gurantees about timezone?) to being a natively readable Unix timestamp (in milliseconds), that way you can avoid problems with Date.parse entirely:

e.g.:

Result.cs:

public class ResultItem
{
    [JsonProperty( "createdOn" )]
    public DateTimeOffset CreatedOn { get; }

    [JsonProperty( "createdOnUnix" )]
    public Int64 CreatedOnUnix => this.CreatedOn.ToUnixTimeMilliseconds();
}

Result.ts:

interface ResultItem {
    readonly createdOn    : string;
    readonly createdOnUnix: number;
}
const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate( ninetyDaysAgo.getDate() - 90 );

const newResult = items.filter( e => new Date( e.createdOnUnix ) < ninetyDaysAgo );

...that way it's a single-line job.


The above can be made even simpler as Unix timestamps are just integers that are directly comparable, so new Date() can be avoided inside the filter, like so:

const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate( ninetyDaysAgo.getDate() - 90 );
const ninetyDaysAgoUnix = ninetyDaysAgo.getTime();

const newResult = items.filter( e => e.createdOnUnix < ninetyDaysAgoUnix );

CodePudding user response:

Assuming you have interfaces defined like this...

interface Item {
  Name: string,
  Description: string,
  CreatedOn?: string // note the optional "?"
}

interface Result {
  items?: Item[] // also optional according to your code
}

and you want to filter for items that are older than 90 days (excluding those with no CreatedOn), then try this

interface ItemWithDate extends Omit<Item, "CreatedOn"> {
  CreatedOn?: Date // real dates, so much better than strings
}

const result: Result = { /* whatever */ }

const items: ItemWithDate[] = result.items?.map(({ CreatedOn, ...item }) => ({
  ...item,
  CreatedOn: CreatedOn ? new Date(CreatedOn) : undefined
})) ?? []

const dateThreshold = new Date()
dateThreshold.setDate(dateThreshold.getDate() - 90)

const newItems = items.filter(({ CreatedOn }) =>
  CreatedOn && CreatedOn < dateThreshold)

TypeScript Playground Demo

CodePudding user response:

code is missing ) of filter function

var currentDate = new Date();
var newResult = result.items?.filter(function(i) {
    return ((currentDate - Date.parse(i.CreatedOn)) > 90);
}  ) //<= misss


  • Related