0

I would like to run functions dynamically given an array function names, but TS has a complain on duration[unit] with the following message:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Duration'.

No index signature with a parameter of type 'string' was found on type 'Duration'.ts(7053)
const duration: Duration = moment.duration(5000)
const unitsOfTime = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']
const timeBreakdown = unitsOfTime.map((unit) => duration[unit]())
Mysterywood
  • 1,378
  • 2
  • 10
  • 19
  • Does this answer your question? [how to dynamically call instance methods in typescript?](https://stackoverflow.com/questions/56894440/how-to-dynamically-call-instance-methods-in-typescript) – Matt U May 30 '21 at 16:58
  • Not really. My code would be `duration[unit as keyof Duration]()` if I follow the suggestion provided in the answer above which still does not work. – Mysterywood May 30 '21 at 17:03

3 Answers3

0

You have to add as const to unitsOfTime, because TS infers it as a stirng[] and duration is not indexed object.

Take a look on Duration interface:

  interface Duration {
    clone(): Duration;

    humanize(argWithSuffix?: boolean, argThresholds?: argThresholdOpts): string;
    
    humanize(argThresholds?: argThresholdOpts): string;

    abs(): Duration;

    as(units: unitOfTime.Base): number;
    get(units: unitOfTime.Base): number;

    milliseconds(): number;
    asMilliseconds(): number;

    seconds(): number;
    asSeconds(): number;

    minutes(): number;
    asMinutes(): number;

    hours(): number;
    asHours(): number;

    days(): number;
    asDays(): number;

    weeks(): number;
    asWeeks(): number;

    months(): number;
    asMonths(): number;

    years(): number;
    asYears(): number;

    add(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration;
    subtract(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration;

    locale(): string;
    locale(locale: LocaleSpecifier): Duration;
    localeData(): Locale;

    toISOString(): string;
    toJSON(): string;

    isValid(): boolean;

    /**
     * @deprecated since version 2.8.0
     */
    lang(locale: LocaleSpecifier): Moment;
    /**
     * @deprecated since version 2.8.0
     */
    lang(): Locale;
    /**
     * @deprecated
     */
    toIsoString(): string;
  }

You are allowed to use only existing props as indexes.

When you add as const, TS will allow you to use unitsOfTime values as a Durarion index

import * as moment from 'moment'

const duration = moment.duration(5000)
const unitsOfTime = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'] as const
const timeBreakdown = unitsOfTime.map((unit) => duration[unit]())

`

0

After consulting some friends and more thoughts, I realized that mapping through an array of string is simply not a very good idea as these are magic strings and it implies that the next person who look at this has a good understanding of what method the Duration object responds to.

A better way to do this is via an explicit configuration mapping

// First we define what should unit of time look like
interface unitOfTimeMap {
  unit: string,
  shortUnit: string,
  amount: (duration: Duration) => number
}

// Then we have an explicit configuration of what unit of time we and how they should be mapped to the duration object.

const unitsOfTimeMapping: unitOfTimeMap[] = [
  {
    unit: 'years',
    amount: (duration: Duration): number => duration.years()
  },
  {
    unit: 'months',
    amount: (duration: Duration): number => duration.months()
  },
  {
    unit: 'weeks',
    amount: (duration: Duration): number => duration.weeks()
  },
  {
    unit: 'days',
    amount: (duration: Duration): number => duration.days()
  },
  {
    unit: 'hours',
    amount: (duration: Duration): number => duration.hours()
  },
  {
    unit: 'minutes',
    amount: (duration: Duration): number => duration.minutes()
  },
  {
    unit: 'seconds',
    amount: (duration: Duration): number => duration.seconds()
  },
]

// Now TS knows exactly what we're working with and no longer complains
const duration: Duration = moment.duration(5000);
const timeBreakdown = unitsOfTimeMapping.map((unitOfTime) => unitOfTime.amount(duration))
Mysterywood
  • 1,378
  • 2
  • 10
  • 19
-1

You can try below solution for moment. Use get method of moment duration and unit as unitOfTime.Base

import moment, { Duration , unitOfTime } from "moment-timezone";


const duration: Duration = moment.duration(5000);
const unitsOfTime: unitOfTime.Base[] = [
  "years",
  "months",
  "weeks",
  "days",
  "hours",
  "minutes",
  "seconds",
];
const timeBreakdown = unitsOfTime.map((unit:unitOfTime.Base) =>  duration.get(unit));

console.log(timeBreakdown);
LogicBlower
  • 1,250
  • 1
  • 8
  • 14