1

I'm struggling with type checking of TypeScript. For example following code:

export function deepClone<T>(obj: T): T { // make sure that deepClone generates the same type as obj
  if (obj == null || typeof obj !== 'object') {
    return obj;
  }

  switch (Object.prototype.toString.call(obj)) {
    case '[object Array]': {
      const result = new Array(obj.length);
      for (let i=0; i<result.length; ++i) {
        result[i] = deepClone(obj[i]);
      }
      return result as any as T;
    }

    // Object.prototype.toString.call(new XxxError) returns '[object Error]'
    case '[object Error]': {
      const result = new obj.constructor(obj.message);
      result.stack = obj.stack; // hack...
      return result;
    }

    case '[object Date]':
    case '[object RegExp]':
    case '[object Int8Array]':
    case '[object Uint8Array]':
    case '[object Uint8ClampedArray]':
    case '[object Int16Array]':
    case '[object Uint16Array]':
    case '[object Int32Array]':
    case '[object Uint32Array]':
    case '[object Float32Array]':
    case '[object Float64Array]':
    case '[object Map]':
    case '[object Set]':
      return new obj.constructor(obj);

    case '[object Object]': {
      const keys = Object.keys(obj);
      const result: any = {};
      for (let i=0; i<keys.length; ++i) {
        const key = keys[i];
        result[key] = deepClone(obj[key]);
      }
      return result;
    }

    default: {
      throw new Error("Unable to copy obj! Its type isn't supported.");
    }
  }
}

I get errors on const result = new Array(obj.length). I know obj's type is any[] but ts compiler fails to recognize it. I have to write the ugly const tmp = obj as any as any[] but it results in extra useless code generation, or I have to write obj as any as whatever in every line that uses obj

Writing function deepClone<T extends any>(obj: T): T works but it disables most type checking.

Another case:

const el = document.getElementById('sth');
switch (el.tagName) {
  case 'INPUT': // Now I know el is a HTMLInputElement element
    el.value = '123'; // Error: HTMLElement doesn't contain property 'value'
    (el as HTMLInputElement).value = '123'; // works
    (el as HTMLInputElement).valueAsNumber = 123; // again
    (el as HTMLInputElement).valueAsDate = xxx; // unacceptable
Carter Li
  • 149
  • 12

1 Answers1

0

If you want to have get closer to assertion free code, you will need to use type guards. There are several types of possible type guards, but switching on the result of toStringis not one of them. A version that uses instanceof and a custom type guard could be:

export function deepClone<T
    extends
    number | string | boolean |
    Array<any> |
    Error |
    Date | RegExp |
    Int8Array | Uint8Array | Uint8ClampedArray |
    Int16Array | Uint16Array |
    Int32Array | Uint32Array |
    Float32Array | Float64Array | Map<any, any> | Set<any>>(obj: T): T { // make sure that deepClone generates the same type as obj
    if (obj == null || typeof obj !== 'object') {
        return obj;
    }

    if (obj instanceof Array) {
        const result = new Array(obj.length);
        for (let i = 0; i < result.length; ++i) {
            result[i] = deepClone(obj[i]);
        }
        return result as any as T;
    }
    else if (obj instanceof Error) {
        const constructor = obj.constructor as new (p: any) => T & Error;
        const result = new constructor(obj.message);
        result.stack = obj.stack;
        return result;
    }
    else if (obj instanceof Date || obj instanceof RegExp ||
        obj instanceof Int8Array || obj instanceof Uint8Array || obj instanceof Uint8ClampedArray ||
        obj instanceof Int16Array || obj instanceof Uint16Array ||
        obj instanceof Int32Array || obj instanceof Uint32Array ||
        obj instanceof Float32Array || obj instanceof Float64Array || obj instanceof Map || obj instanceof Set) {

        const constructor = obj.constructor as new (p: T) => T;
        return new constructor(obj);
    }
    else if (isObject(obj)) {
        const keys = Object.keys(obj);
        const result: any = {};
        for (let i = 0; i < keys.length; ++i) {
            const key = keys[i];
            result[key] = deepClone(obj[key]);
        }
        return result;
    } else {
        throw new Error("Unable to copy obj! Its type isn't supported.");

    }

    function isObject(obj: object | T): obj is { [k: string]: any } {
        return typeof obj === 'object'
    }
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Isn't `Object#toString.call` with switches (and maybe `slice(8, -1)`) results in cleaner and less code than `instanceof`s? – Carter Li Oct 20 '18 at 10:10
  • quick benchmark: `instanceof` is about 15% slower than `Object#toString.call` on my machine (Chrome 70) https://jsperf.com/object-prototype-tostring-call-vs-instanceof/1 – Carter Li Oct 20 '18 at 10:29
  • @CarterLi didn't say it was faster, just that Typescript does not understand your version to be a type guard. – Titian Cernicova-Dragomir Oct 20 '18 at 10:59
  • 1
    I decided to write a d.ts file and keep the original js version. Thanks for your comments. – Carter Li Oct 20 '18 at 11:49