"this" not updated inside TypeScript class - javascript

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.

Related

TypeScript Mixins and Constructor Names

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.

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

Typescript Class-Validator Contains multiple strings (IsEmail)

I have been using the class-validator decorator library to validate dto objects and would like to create a domain whitelist for the #IsEmail decorator. The library includes a #Contains decorator, which can be used for a single string (seed), but I would like to input an array of valid strings instead. Is this possible?
current
#Contains('#mydomain.com')
#IsEmail()
public email: string;
desired
#Contains(['#mydomain, #yourdomain, #wealldomain, #fordomain'])
#IsEmail()
public email: string;
If this is not possible, I would appreciate any direction on how to implement a custom decorator which serves this purpose.
Thanks.
Consider this example:
const Contains = <Domain extends `#${string}`, Domains extends Domain[]>(domains: [...Domains]) =>
(target: Object, propertyKey: string) => {
let value: string;
const getter = () => value
const setter = function (newVal: Domain) {
if (domains.includes(newVal)) {
value = newVal;
}
else {
throw Error(`Domain should be one of ${domains.join()}`)
}
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
}
class Greeter {
#Contains(['#mydomain', '#wealldomain'])
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
const ok = new Greeter('#mydomain'); // ok
const error = new Greeter('234'); // runtime error
Playground
If you provide invalid email domain it will trigger runtime error. It is up to you how you want to implement validation logic. It might be not necessary a runtime error

TypeScript: Getting the type signature of values in a dynamically keyed object

I'd like to reference the value type in a type object I'm importing from a library and do not control.
I am using the serverless TypeScript package and it provides a type called AWS that looks like this:
{
...
functions?: {
[k: string]: {
name: string;
...
}
}
}
I know can reference the type of the entire functions sub-object with AWS["functions"]
My question is, how can I programmatically ask for the sub-type of the values of that sub-object?
See the code below with "TYPE_HERE" indicating where I want to fill in the type.
import type { AWS } from "#serverless/typescript";
export default (): TYPE_HERE => ({
name: "",
...
});
How can I state the type using the root type, or even the type AWS["functions"], when it exists as the value property of a dynamic key?
This should work:
interface AWS {
functions?: {
[k: string]: {
name: string;
}
}
}
type FnType
= NonNullable<AWS["functions"]> extends { [K: string]: infer R } ? R : never
const d = (): FnType => ({
name: "",
dd: '', // error
});
TS playground

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

Categories

Resources