Search code examples
typescriptjsdoc

How do I deal with type casting by property assignment when transforming with typescript/jsdoc?


type FromType = {
  // [...]
  value: string;
}

type ToType = Omit<FromType, 'value'> & {
  value: number;
}

then

/**
 * @param {FromType[]} records
 * @returns {ToType[]} records
 */
function transform(records){
  return records.map((record) => {
    record.value = parseFloat(record.value);
    
    return record;
  });
}

Complains because record.value is expected to be a string. A workaround is to use Object.assign

/**
 * @param {FromType[]} records
 * @returns {ToType[]} records
 */
function transform(records){
  return records.map((record) => {
    return Object.assign(record, {
      value: parseFloat(record.value),
    });
  });
}

How can I handle type casting on assignment?

Assume:

  • It's safe to mutate.
  • I don't want to define an incorrect input property having string | number.
  • I don't want to make new objects for performance reasons i.e. { ...item, value: 0 };

Solution

  • With apologies, I don't know how to annotation this with JSDoc, but with TypeScript's own type annotations in order to perform that mutation you'd briefly lie to TypeScript to trick it into allowing it:

    type FromType = {
        // [...]
        value: string;
    };
    
    type ToType = Omit<FromType, "value"> & {
        value: number;
    };
    
    type X = ToType["value"];
    
    /**
     * @param   records The record to transform.
     * @returns Transformed records.
     */
    function transform(records: FromType[]): ToType[] {
        return records.map((record) => {
            const result = record as any as ToType;     // A brief lie...
            result.value = parseFloat(record.value);    // ...which is now true
            return result;
        });
    }
    

    Live example on the playground

    If you could avoid mutation, I would strongly encourage it (it would then just be return records.map((value, ...rest) => ({...rest, value: parseFloat(value)}));), but if you can't avoid it, you can't avoid it. :-)

    Since you're mutating the objects, unless you have a very unusual use case, there's no reason to create a new array, and not doing so may also help with your performance issue:

    /**
     * @param   records The record to transform.
     * @returns The **same** array, objects are mutated in place.
     */
    function transform(records: FromType[]): ToType[] {
        for (let n = records.length - 1 ; n >= 0; --n) {
            const record = records[n];
            const result = record as any as ToType;     // A brief lie...
            result.value = parseFloat(record.value);    // ...which is now true
        }
        return records as any as ToType[];
    }
    

    Playground link

    Even if you do need to create a new array, given that this function is mutating in place, it probably makes sense to have that be something the caller does (perhaps via slice, which is very fast).