I am translating a program from c++ to typescript, and I face a strange behaviour trying to empty an array using the splice technique (How do I empty an array in JavaScript?) to empty an array.
Here is an excerpt of my code in typescript
"use strict"
class UniformGridGeometry<ItemT> extends Array<ItemT> {
itemType: { new (): ItemT; }
constructor(itemType: { new (): ItemT; }) {
// constructor(itemType: { new (): ItemT; }, uGeomTemplate: UniformGridGeometry<any>) // any : Vorton, Particle, Vec*, Mat*, ...
// constructor(itemType: { new (): ItemT; }, uNumElements: number, vMin: Vec3, vMax: Vec3, bPowerOf2: boolean)
// constructor(itemType: { new (): ItemT; }, arg?: any, vMin?: Vec3, vMax?: Vec3, bPowerOf2?: boolean) {
super(); // Array
this.itemType = itemType;
// (...)
}
}
class UniformGrid<ItemT> extends UniformGridGeometry<ItemT> {
constructor(itemType: { new (): ItemT; }) {
// constructor(itemType: { new (): ItemT; }, uGeomTemplate: UniformGridGeometry<any>) // any : Vorton, Particle, Vec*, Mat*, ...
// constructor(itemType: { new (): ItemT; }, uNumElements: number, vMin: Vec3, vMax: Vec3, bPowerOf2: boolean)
// constructor(itemType: { new (): ItemT; }, arg?: any, vMin?: Vec3, vMax?: Vec3, bPowerOf2?: boolean) {
super(itemType);
// (...)
}
}
class NestedGrid<ItemT> extends Array<UniformGrid<ItemT>> {
constructor(src?: UniformGrid<ItemT>) {
super();
if (src) {
this.Init(src);
}
}
Init(src: UniformGrid<ItemT>) {
this.splice(0, this.length) // mUniformGrids.Clear() ;
console.assert(typeof src === 'object', typeof src);
// let numUniformGrids = this.PrecomputeNumUniformGrids( src ) ;
// this.mUniformGrids.Reserve( numUniformGrids ) ; // Preallocate number of UniformGrids to avoid reallocation during PushBack.
let uniformGrid = new UniformGrid<ItemT>(src.itemType);
// uniformGrid.Decimate( src , 1 ) ;
// uniformGrid.Init() ;
this.push(uniformGrid);
// (...)
}
}
function doTests() {
console.info("Test > NestedGrid ; UniformGrid");
let mInfluenceTree: NestedGrid<any> = new NestedGrid<any>(); // Influence tree
let ugSkeleton = new UniformGrid<any>(null);
mInfluenceTree.Init(ugSkeleton);
console.log(mInfluenceTree);
mInfluenceTree.Init(ugSkeleton);
console.log(mInfluenceTree);
}
doTests();
that generates (ES6 target) the following Javascript :
"use strict";
class UniformGridGeometry extends Array {
constructor(itemType) {
super();
this.itemType = itemType;
}
}
class UniformGrid extends UniformGridGeometry {
constructor(itemType) {
super(itemType);
}
}
class NestedGrid extends Array {
constructor(src) {
super();
if (src) {
this.Init(src);
}
}
Init(src) {
this.splice(0, this.length);
console.assert(typeof src === 'object', typeof src);
let uniformGrid = new UniformGrid(src.itemType);
this.push(uniformGrid);
}
}
function doTests() {
console.info("Test > NestedGrid ; UniformGrid");
let mInfluenceTree = new NestedGrid();
let ugSkeleton = new UniformGrid(null);
mInfluenceTree.Init(ugSkeleton);
console.log(mInfluenceTree);
mInfluenceTree.Init(ugSkeleton);
console.log(mInfluenceTree);
}
doTests();
The same code, on firefox or as code snippet works well, but on chromium the assertion fails, the argument 'src' becomes a number (the size of the array in fact) what am I doing wrong ?
(the two Init calls simulates the processing in the WebGL loop)
chromium splice failing
Thanks.
It looks like splice, which creates a new array to return the deleted elements, reuses the class of the element it's called on, hence calling your custom constructor with the desired size.
Here you can fix the problem by using the other way to empty an array: setting its size to 0. Replace
this.splice(0, this.length);
with
this.length = 0;
Another solution might have been to respect the contract of the class you extends as Array's constructor has a different behavior than the one you're implementing in the subclass.
Note that you're in a grey area, regarding specifications. It's probably wiser to avoid extending basic classes like Array.
Related
I am currently working on a Vue3 project where we use classes.
Since axios does not return class instances, we need to transform them and sometimes we even need to transform some properties.
Now I am working on an ActivityLog where I have a function convertChildrenToClasses:
export class ActivityLog extends BaseModel {
user?: any = undefined
activity = ''
activityResource = ''
oldValues: Record<string, string> = {}
newValues: Record<string, string> = {}
createdAt = ''
constructor() {
super();
}
get createdAtStr() {
if (this.createdAt) {
return moment(this.createdAt).format(dateTimeFormatString);
}
return null;
}
convertChildrenToClasses = () => {
console.log(this.user); // returns undefined, although previously filled
if (this.user) {
this.user = convertToClass<User>(new User(), {...this.user});
}
}
}
As you can see, we also have a method that recursively transforms the classes and their children, which is called before the previous one:
export function convertToClass<T>(emptyObject: T, entity: any): T {
const entityClass = Object.assign(emptyObject, entity);
if (entityClass.convertChildrenToClasses) {
if (entityClass instanceof ActivityLog) {
console.log(entityClass.user); // Returns an object with values and is not manipulated anywhere else
}
entityClass.convertChildrenToClasses() // console.log inside the class method returns undefined
}
return entityClass as T;
}
Now iam wondering, why the console.log() in convertToClass returns an object with values:
{#id: '/api/users/7', #type: 'User', id: 7, email: 'artur.smolen#explicatis.com', firstName: 'Artur', …}
and the console.log() in convertChildrenToClasses returns undefined
This is the method, where all starts:
function pushToCollection(entities: T[]) {
collection.value.push(...entities.map(entity => convertToClass(Object.create(initialValue.value), entity)))
}
In the current case initialValue has new ActivityLog() as value and the entities are the server-reponse
I've some code like:
const methodsList = [
'foo',
'bar',
// ... 20 other items ...
]
export class Relayer {
constructor() {
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// console.log('relaying call to', methodName, args)
// this is same for all methods
}
}
}
}
const relayer = new Relayer()
relayer.foo('asd') // TS error
relayer.bar('jkl', 123) // TS error
Now when I use the class instance, TypeScript complains when I call relayer.foo() or relayer.bar(). To make the code compile, I've to cast it as any or similar.
I've an interface that declares foo, bar and the other methods:
interface MyInterface {
foo: (a: string) => Promise<string>
bar: (b: string, c: number) => Promise<string>
// ... 20 other methods
}
How do I get TypeScript to learn the dynamically declared foo and bar class methods? Can the declare syntax be useful here?
First step is to create a type or interface where when indexed by a value in methodsList, the result will be a function:
// The cast to const changes the type from `string[]` to
// `['foo', 'bar']` (An array of literal string types)
const methodsList = [
'foo',
'bar'
] as const
type HasMethods = { [k in typeof methodsList[number]]: (...args: any[]) => any }
// Or
type MethodNames = typeof methodsList[number] // "foo" | "bar"
// k is either "foo" or "bar", and obj[k] is any function
type HasMethods = { [k in MethodNames]: (...args: any[]) => any }
Then, in the constructor, to be able to assign the keys of methodsList, you can add a type assertion that this is HasMethods:
// General purpose assert function
// If before this, value had type `U`,
// afterwards the type will be `U & T`
declare function assertIs<T>(value: unknown): asserts value is T
class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
// `methodName` has type `"foo" | "bar"`, since
// it's the value of an array with literal type,
// so can index `this` in a type-safe way
this[methodName] = (...args) => {
// ...
}
}
}
}
Now after constructing, you have to cast the type still:
const relayer = new Relayer() as Relayer & HasMethods
relayer.foo('asd')
relayer.bar('jkl', 123)
You can also get rid of the casts when constructed using a factory function:
export class Relayer {
constructor() {
// As above
}
static construct(): Relayer & HasMethods {
return new Relayer() as Relayer & HasMethods
}
}
const relayer = Relayer.construct()
Another way around it is to create a new class and type-assert that new results in a HasMethods object:
class _Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
}
export const Relayer = _Relayer as _Relayer & { new (): _Relayer & HasMethods }
const relayer = new Relayer();
relayer.foo('asd')
relayer.bar('jkl', 123)
Or if you are only using new and then methods in methodsList, you can do:
export const Relayer = class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
} as { new (): HasMethods };
You can also use your MyInterface interface instead of HasMethods, skipping the first step. This also gives type-safety in your calls.
Use the following syntax:
export class Relayer {
constructor() {}
public foo(){
// your foo method
this.executedOnEachFunction();
}
public bar(){
// your bar method
this.executedOnEachFunction();
}
executedOnEachFunction(){
// what you want to do everytime
}
}
https://repl.it/repls/LawfulSurprisedMineral
To me, this sounds like a need for an interface.
interface MyInterface {
foo(): void; // or whatever signature/return type you need
bar(): void;
// ... 20 other items ...
}
export class Relayer implements MyInterface {
constructor() {}
foo(): void {
// whatever you want foo to do
}
// ... the rest of your interface implementation
}
What it looks like you are doing is implementing some interface of sorts. In your constructor you are defining what the method implementations are instead of defining them in the class body. Might help to read Class Type Interfaces
I'm struggling to define how to write TypeScipt code which says that function return constructor of generic type. There are plenty of examples around about how to pass constructor of the generic type, but not how to return.
Please check the following example:
This is part of the abstract class:
getModel(): (new () => T) {
throw new Error('Method not implemented.'); // Error because don't know how to fix it
}
When in derived class I'm trying to implement it like this:
getModel(): typeof User {
return User;
}
I have the following error:
Type '() => typeof User' is not assignable to type '() => new () => User'.
I could skip implementation in the derived class if I knew how to specify in the abstract class.
So question is - how to specify on an abstract class level that method returns constructor of the generic type and I can skip implementation of this method at child level class? Or maybe I specify return signature not correctly on the abstract class level?
EDIT:
Please check the strange problem. Class A and B differ only by the presence of explicit constructor. And in RealA doesn't work and RealB works the same getModel() method.
class A {
a = '';
constructor(a: string) {
}
}
class B {
a = '';
static test(): void {
console.log('I do work');
}
}
abstract class Base<T> {
Prop: T;
constructor(TCreator: { new (): T; }) {
this.Prop = new TCreator();
}
getModel(): (new () => T) {
throw new Error('Method not implemented.'); // Error because don't know how to fix it
}
}
class RealA extends Base<A> {
getModel(): typeof A { // doesn't work - compilation error
return A;
}
}
class RealB extends Base<B> {
getModel(): typeof B { // works
return B;
}
}
var test = new RealA(A); // compile error
var test2 = new RealB(B)
For RealA class the same error
() => typeof A' is not assignable to type '() => new () => A'
The error is expected as the constructor for class A has a required argument. The abstract class constrains the constructor to be passed it to have no arguments (new () => T).
The simple solution is to remove the constructor to A.
If you want to be able to pass in classes that have constructors that require arguments you will need to change the definition of the base class to capture the constructor type, and have the constructor take in those required arguments (using tuples in rest parameters)
class A {
a = '';
constructor(a: string) {
}
}
class B {
a = '';
static test(): void {
console.log('I do work');
}
}
type ArgumentTypes<T> = T extends new (...a: infer A) => any? A : []
abstract class Base<T extends new (...a: any[])=> any> {
Prop: InstanceType<T>;
constructor(TCreator: T, ...a: ArgumentTypes<T>) {
this.Prop = new TCreator(...a);
}
getModel(): T {
throw new Error('Method not implemented.'); // Error because don't know how to fix it
}
}
class RealA extends Base<typeof A> {
getModel(): typeof A { // doesn't work - compilation error
return A;
}
}
class RealB extends Base<typeof B> {
getModel(): typeof B { // works
return B;
}
}
var test = new RealA(A, ""); // ok
var test2 = new RealB(B)
I have a method that returns different type based on option key value.
class Test {
getData(options: { obj: true }): Object;
getData(options: { obj: false }): any[];
getData(): any[];
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
When passing the obj as true, I'm returning object otherwise array. That works fine.
const instance = new Test();
const result = instance.getData({ obj: true }); // inffered as array
const result2 = instance.getData(); // inffered as object
The problem is when I need to use dynamic value it throws an error:
type boolean is not assignable to type false
function getResult(obj: boolean = false ) {
return instance.getData({ obj });
}
What is the problem?
Since the type of { obj } is only known as { obj: boolean } at compile time, the compiler can't know to pick any of the overloads, you have to explicitly supply an overload that takes { obj: boolean } (since the implementation signature does not count as a public signature for the function), the compiler will not do any magic in this case:
class Test {
getData(options: { obj: true }): Object;
getData(options: { obj: false }): any[];
getData(options: { obj: boolean }): Object | any[];
getData(): any[];
// This signature is the implementation and is not conidered when resolving the method
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
Edit
You can also use conditional types in the method signature and this will keep the number of signatures lower:
class Test {
getData<T extends boolean>(options: { obj: T }): T extends true ? Object : any[];
getData(): any[];
// This signature is the implementation and is not conidered when resolving the method
getData(options = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
const instance = new Test();
const result = instance.getData({ obj: true }); // inffered as array
const result2 = instance.getData(); // inffered as object
function getResult(obj: boolean = false) {
return instance.getData({ obj }); // inferred as Object|any[]
}
Since type boolean = true | false and conditional types distribute over unions,
T extends true ? Object : any[]; will be Object|any[] when T is boolean. When T is true, the return will be Object and when T is false, the return will be any all as expected
You cannot overload methods in TypeScript like you do in C#, for example. You need to combine types, like this:
class Test {
getData(options: {obj: boolean} = { obj: false }): Object | any[] {
if (options.obj) {
return {};
} else {
return [];
}
}
}
When you redeclare the function with the same name several times, you will just get the last one as the final definition, in runtime.
I'm on a web application that I write in TypeScript. In one part of the application, the use can write an additional JavaScript function, that will be parsed at runtime (new function(Function as String)) for execution. The code will give me back an object which I defined in TypeScript as class Script. This script object should contain specific functions, otherwise it's invalid. Meaning, I want to throw an exception, if one or more functions are not implemented by the user.
A Typescript cast won't do it, as it is a compile time cast.
I thought about giving the Script object a constructor that takes the parsed object (that, by key/values, should be a Scriptobject already) and check the object in the constructor for missing properties.
Something like this:
(This will not work, it only should show the idea)
export class Script {
constructor(object: Script) {
this.getDefaultValue = object.getDefaultValue;
this.isAvailable = object.isAvailable;
this.isValid = object.isValid;
this.isInRange = object.isInRange;
this.isDataFormat = object.isDataFormat;
for (let propertie in this){
if (!this[propertie]){
throw new Error(propertie+ ' is missing.');
}
}
}
getDefaultValue: any;
isAvailable: (containerSetId: number) => boolean;
isValid: (value: any) => boolean;
isInRange: (value: any) => any;
isDataFormat: (value: any) => boolean;
}
But isn't there a nicer way to do this?
You can't use that because:
class A {
member1: string;
member2: number;
member3: boolean;
constructor() {
this.member3 = true;
}
}
Compiles into:
var A = (function () {
function A() {
this.member3 = true;
}
return A;
}());
As you can see, the member1 and member2 are not part of the compiled js version.
You'll need to keep track of the required properties for runtime, something like:
class Script {
getDefaultValue: any;
isAvailable: (containerSetId: number) => boolean;
isValid: (value: any) => boolean;
isInRange: (value: any) => any;
isDataFormat: (value: any) => boolean;
required = [
"getDefaultValue",
"isAvailable",
"isValid",
"isInRange",
"isDataFormat"
]
constructor(object: Script) {
this.getDefaultValue = object.getDefaultValue;
this.isAvailable = object.isAvailable;
this.isValid = object.isValid;
this.isInRange = object.isInRange;
this.isDataFormat = object.isDataFormat;
for (let propertie in this.required) {
if (!this[propertie] || typeof this[propertie] !== "function") {
throw new Error(propertie+ ' is missing.');
}
}
}
}