TypeScript Mixins and Constructor Names - javascript

I have this sample of code experimenting with mixins in TypeScript. However, it is not returning what I am expecting.
It should give me: User ({"id":3,"name":"Lorenzo Delaurentis"}).
Instead, I am getting: Function ({"id":3,"name":"Lorenzo Delaurentis"}).
The line let Name = Class.constructor.name should give me User, but it is not. Am I missing something obvious here?
type ClassConstructor<T> = new(...args: any[]) => T
function withDebug<C extends ClassConstructor<{
getDebugValue(): object
}>>(Class: C) {
return class extends Class {
constructor(...args: any[]) {
super(...args)
}
debug() {
let Name = Class.constructor.name
let value = this.getDebugValue()
return `${Name} (${JSON.stringify(value)})`
}
}
}
class DebugUser {
constructor(
private id: number,
private firstName: string,
private lastName: string
) {}
getDebugValue() {
return {
id: this.id,
name: `${this.firstName} ${this.lastName}`
}
}
}
let User = withDebug(DebugUser)
let user = new User(3, 'Lorenzo', "Delaurentis")
console.log(user.debug())
P.S. I compiled with tsc mixins --target ES6. Otherwise, I get an error: error TS2339: Property 'name' does not exist on type 'Function'.

You want just Class.name. The Class.constructor is Function.

Related

How to implement a Typescript interface that allows additional properties?

Ok, I have been struggling with this one as all information I find is about how to define interfaces that allow other properties, but not how to create a class that can implement the interface.
I have (or want to have) the following interface:
export interface IEnvironment {
value: string;
names: string[];
[x: string | 'value' | 'names']: (() => boolean) | string | string[]
};
Then I want a class that implements said interface, but I only want to implement the value and names properties.
For full disclosure, I want to create an environment object with value, names and one function per name in names. Like this:
export class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};
Now this works except for one error I get:
Class 'Environment' incorrectly implements interface 'IEnvironment'.
Index signature for type 'string' is missing in type 'Environment'.
So how can I do this? I'm a noob in the TypeScript arena, so I'd rather ask the experts.
If no solution, could this be worked around with another interface that extends IEnvironment? Like remove the extra properties thing and move it to another interface that I would use as consumer of the object so I get the correct Intellisense.
Thank you very much in advance.
You just need to declare this dynamic x property from interface as class property.
Add this line as your class property: [x: string]: string|(() => boolean)|string[];
Finally, your class looks like this:
class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
//ADD THIS
[x: string]: string|(() => boolean)|string[];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};

ImmutableJS Records won't detect interface with Typescript

I'm trying to get running Typescript with Immutable 3.8.1.
import { Record } from "immutable";
class Person extends Record({
name: "viktor" as string,
}) {
getName() {
return this.name;
}
}
const person = new Person({ name: "petko" });
console.log(person.getName());
this code produces the following error:
Property 'name' does not exist on type 'Person'.
5 | }) {
6 | getName() {
> 7 | return this.name;
| ^^^^
8 | }
9 | }
but it does compile and run. However, when I try to add name: string; just over the getName declaration (in the class body), the error goes away, but getName() returns undefined every time. What I'm doing wrong?
I'm not that advanced in TS, but if I got it right, the problem is that although the property name does exist on type Person, the TypeScript compiler doesn't know about it, because it's created dynamically at runtime.
So, you have two ways:
Use the immutable record's get function to read the value:
import { Record } from "immutable";
class Person extends Record({
name: "viktor" as string,
}) {
getName() {
return this.get("name");
}
}
const person = new Person({ name: "petko" });
console.log(person.getName());
Cast to any:
import { Record } from "immutable";
class Person extends Record({
name: "viktor" as string,
}) {
getName() {
return (<any> this).name;
}
}
const person = new Person({ name: "petko" });
console.log(person.getName());

Typescript complaining about not assigning a get property

I have this code stackblitz
export class Student {
id: number;
name: string;
age?:number;
get studentType():string {
return 'fullTime'
}
constructor(params: Student) {
Object.assign(this, params);
}
}
const student = new Student({id:1, name: 'Jon'}); //ts error here
I get the below error
Argument of type '{ id: number; name: string; }' is not assignable to parameter of type 'Student'.
Property 'studentType' is missing in type '{ id: number; name: string; }'.
While studentType is a get only property and can't bet set.
What is the reason for that and how can I solve it?
ps. (I don't want to make it nullable like studentType? or convert it to just a function)
Getters / Setters are exactly like regular properties, thats why Typescript can't distinguish between a getter / setter and a regular property. You could make all properties optional though, with that you can omit the studentType:
constructor(params: Partial<Student>) {
Object.assign(this, params);
}
However other properties (e.g. name) could also be omitted now. To make that more typesafe you could introduce an additional interface:
export interface StudentData {
id: number;
name: string;
age?:number;
}
export class Student implements StudentData {
get studentType():string {
return 'fullTime'
}
constructor(params: StudentData) {
Object.assign(this, params);
}
}
That is a more controversial topic in TypeScript.
For class, TypeScript consider the overall shape of the class to be the type.
This includes private variables and methods, and in this case, including the getter/setter.
One solution to your problem is you can use Partial<>
constructor(params: Partial<Student>) { ... }
or Pick<>
constructor(params: Pick<Student, 'id' | 'name' | 'age'>) { ... }
Another way is to create an interface yourself:
interface IStudent { id: number, name: string, age?:number }
class Student implements IStudent {
constructor(params: IStudent) { ... }
}
What is the reason for that?
Basically, {id:1, name: 'Jon'} is not a student, since that object lacks a studentType property. This seems obvious and idiotic but makes sense, since typescript cannot know wether you're gonna rely on that property of the argument or not.
In your constructor, you just call Object.assign and let it go. But you could be calling some function that actually relies on the argument having that property, which could led to a runtime error if not pointed out by typescript.
and how can I solve it?
Well, there are several answers already. I would just type the constructor parameter properly. If you expect an object that has id, name and/or age I would type it accordingly:
export class Student {
id: number;
name: string;
age?:number;
get studentType():string {
return 'fullTime'
}
constructor(params: {id: number, name: string, age?: number}) {
Object.assign(this, params);
}
}
const student = new Student({id:1, name: 'Jon'}); //ts error here
This is because you are giving the type in constructor as Student.
This can be done:
export class Student {
id: number;
name: string;
age?: number;
constructor(id:number, name:string,age?:number) {
this.age = age;
this.name = name;
this.id = id;
}
get studentType():string {
return 'fullTime'
}
}
const student = new Student(1, 'Jon');

"this" not updated inside TypeScript class

I'm trying to add an object of type Errorto an array using a method. So here is my code:
export type Error{
types: string[];
message: string;
}
export class Form {
[k: string]: any;
id: string;
errors: Error[];
constructor(obj?: Form) {
this.id = '';
if (obj) {
Object.assign(this, obj);
if (obj.errors) {
this.errors = obj.errors.map(x => ({...x}));
}
}
}
public addError = (error: Error) => {
this.errors.push(error);
}
}
If I do create a new Form and add an Error to it, errors array is not updated!!!
const form1 = {errors: []};
const form2 = new Form(form1);
form2.addError ({types: ['a'], message: 'error'});
//form2.errors.length returns 0
I'm creating a new instance of Form because I want to handle deep copy inside my constructor.
Am I missing something here?
There are several issues with your code, which should be showing up as compile-time errors:
The type keyword expects the name following it to be followed by an equals sign. If it were a class, then you could remove the equals.
If you have a class with methods and want to pass it as a variable, you should either
use new, or
include all defined methods and properties in the object literal you use.
Or, if all you want is the properties, use an interface, as I've done below.
export type Error = {
types: string[];
message: string;
}
export interface IForm {
[k: string]: any;
id: string;
errors: Error[];
}
export class Form implements IForm {
[k: string]: any;
id: string;
errors: Error[];
constructor(obj?: IForm) {
this.id = '';
if (obj) {
Object.assign(this, obj);
if (obj.errors) {
this.errors = obj.errors.map(x => ({...x}));
}
}
}
public addError = (error: Error) => {
this.errors.push(error);
}
}
const form1: IForm = { errors: [], id: 'form1'};
const form2 = new Form(form1);
form2.addError({types: ['a'], message: 'error'});
console.log(form2.errors.length);
I'm not able to reproduce using the code you provided. In this screen capture you can see that form2.errors is an array with the expected 1 element in it. (the error underline for form1 is pointing out that form1 doesn't have an id property):
Your [k: string]: any; is damaging the safety net that TypeScript gives you.
If you didn't have it, you would have gotten a compilation error for adderror not being a thing, and TypeScript would have even suggested addError as the closest valid alternative.

How to do a runtime cast

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.');
}
}
}
}

Categories

Resources