Skip to content

Instantly share code, notes, and snippets.

@jwulf
Created September 24, 2024 11:18
Show Gist options
  • Save jwulf/6e7b093b5b7b3e12c7b76f55b9e4be84 to your computer and use it in GitHub Desktop.
Save jwulf/6e7b093b5b7b3e12c7b76f55b9e4be84 to your computer and use it in GitHub Desktop.
A difference in behavior between Node 22's strip types and TypeScript transpilation.
/**
* This code behaves differently when run with:
*
* node --experimental-strip-types test.ts
*
* tsc --lib es2017,dom test.ts && node test.js
*
* This is an edge-case where mixing dynamic programming and static typing reveals a difference in behavior.
*
*/
export class LosslessDto {
constructor(obj?: any) {
if (obj) {
for (const [key, value] of Object.entries(obj)) {
(this as any)[key] = value
}
}
}
}
class V extends LosslessDto {
orderId!: string
customerId!: string
paymentStatus!: "unpaid" | "paid"
constructor(data: V) {
super(data)
}
}
const v = new V({
orderId: '123',
customerId: '456',
paymentStatus: 'paid',
})
/**
* When run with Node 22 strip types, all properties of v are undefined.
*
* When transpiled, they are defined.
*/
console.log('v', v);
/**
* The cause of this is the property definitions of the class V. These are provided in TypeScript to create strong typing for the constructor parameter.
* Particularly, you can specify whether they are optional or required with the ? and ! operators.
*
* However, when these operators are stripped the property names are left, and they act as property *initializers* in JS, and they are
* applied *after* the call to `super` in the constructor.
*
* This means that the properties are initialized to `undefined` *after* the super constructor dynamically assigns them with the provided values.
*
* In the transpiled code, since nothing is assigned to these properties, they are stripped out of the class definition, and the super constructor
* is able to set them without them being overwritten.
*/
@jwulf
Copy link
Author

jwulf commented Sep 24, 2024

Here it is with stripped types:

class LosslessDto {
	constructor(obj) {
		if (obj) {
			for (const [key, value] of Object.entries(obj)) {
				this[key] = value
			}
		}
	}
}

class V extends LosslessDto {
    orderId
    customerId
    paymentStatus
	constructor(data) {
		super(data)
	}
}

const v = new V({
    orderId: '123',
    customerId: '456',
    paymentStatus: 'paid',
})

console.log('v', v);

@jwulf
Copy link
Author

jwulf commented Sep 24, 2024

And here is the transpiled code:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

var LosslessDto = /** @class */ (function () {
    function LosslessDto(obj) {
        if (obj) {
            for (var _i = 0, _a = Object.entries(obj); _i < _a.length; _i++) {
                var _b = _a[_i], key = _b[0], value = _b[1];
                this[key] = value;
            }
        }
    }
    return LosslessDto;
}());
var V = /** @class */ (function (_super) {
    __extends(V, _super);
    function V(data) {
        return _super.call(this, data) || this;
    }
    return V;
}(LosslessDto));
var v = new V({
    orderId: '123',
    customerId: '456',
    paymentStatus: 'paid',
});

console.log('v', v);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment