For a specific application we are storing id's of objects in specific classes. For example, a "product" object would have it's string id stored in a "ProductId" object. Likewise a "user" object would have it's string id stored in a UserId object (see example code below).
class Product {
id: ProductId;
price: number;
...
}
class User {
id: UserId;
name: string;
...
constructor(id: UserId, name: string) {
this.id = id;
this.name = name;
...
}
}
class ProductId {
id: string;
constructor(id: string) {
this.id = id;
}
}
class UserId {
id: string;
constructor(id: string) {
this.id = id;
}
}
One issue with this approach is that storing objects in a Map and then trying to retrieve them (see below code) does not work because two UserId's with the same underlying id do not compare equal with ===.
const users = new Map<UserId, User>();
const user = new User(new UserId('8753098'), 'John');
users.set(user.id, user);
console.log(users.get(new UserId('8753098')); //undefined
It seems that javascript does not have operator overloading, or has no way of overriding the equality function.
I have also thought of working with a global map, and create Id's with a static method :
class UserId {
private id: string;
constructor(id: string) {
this.id = id;
}
static userIds = new Map<string, UserId>();
static fromString(id: string) {
let userId = userIds.get(id);
if (userId === undefined) {
userId = new UserId(id);
userIds.set(id, userId);
}
return userId;
}
}
But that has a potential memory leak because all objects are retained in the map and never released.
Does anyone have a solution for this ?
Does anyone have a solution for this ?
Instead of class UserId just do a type type UserId = string.
More
If you are concerned about structural equality and would prefer nominal typing you can add a brand using an enum as shown here
enum UserIdBrand {}
type UserId = UserIdBrand & string;
Related
I have many models ( which are javascript classes ) holding names for my database, and also it's types. Is there a way I can create a new User without having to call all those attributes inside the constructor ?
class User {
name: string;
email: string;
password: string;
constructor(values: User) {
this.name = values.name;
this.email = values.email;
this.password = values.password;
// I don't want to call this.name, this.email, ... Since this User model is relatively small, but I have models with hundreds of attributes
}
}
In typescript you can create class fields in the constructor's argument list, like this:
class User {
// nothing here :)
constructor(private name: string, private email: string, private password: string) {
// empty :)
}
}
If you specify an access modifier, you get the declaration and the assignment automatically.
In my project, I have a handful of data model classes that take a response from an API in the constructor.
Here's an example, with an arbitrary toJSON method.
class Document {
id: number
name: string
constructor(init: DocumentStructure) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
It's coming into the constructor as an object, so for correct typing I also have a type definition for the object structure that is separate from the class. Assume that it must come in as an object due to requirements further up the chain.
type DocumentStructure = {
id: number
handle: string
}
My question is: is there any way to use the class as a structural definition? Could I ever do something like the following, where the incoming init object is a JSON structure that matches the attributes of Document, but is not actual an instance of the class Document?
class Document {
id: number
name: string
constructor(init: Document) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
If this is an impossible/bad idea, what are the TS best practices for dealing with this?
I would suggest separating the classes and input structures, e.g. class Document and interface DocumentDTO:
interface DocumentDTO {
id: number
name: string
}
class Document extends DocumentDTO {
constructor(init: DocumentDTO) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
If though you are in some way restricted, you can also use the following approach:
// A helper type for extractig all non-function properties from type C
type SerializableProperties<C> = {
[K in keyof C]: C[K] extends Function ? never : K;
}[keyof C];
// This type will be the same as C but it will not have any function properties
//
// If you don't need the extra generality you can also just say
//
// type Serializable<C> = Omit<C, 'toJSON'>;
//
// But that might give you some headaches when you start adding methods to those classes :)
type Serializable<C> = Pick<C, SerializableProperties<C>>;
class DocumentClass {
id: string;
name: string;
constructor(init: Serializable<DocumentClass>) {
this.id = init.id
this.name = init.name
}
toJSON(): Serializable<DocumentClass> {
return {
id: this.id,
name: this.name
}
}
}
enter link description hereTypeScript Playground link here
If you're okay with keeping all of the class parameters, you can skip the toJSON method and use the built-in
// { id: 'foo', name: 'bar' }
JSON.stringify(new Document(...))
I have a typescript class inheriting another one. I would like to create a factory class that creates an object of one or the other using basic logic, but it is not working.
This is a basic class for a Customer:
class Customer {
static member = true;
id:string;
static c_type = "Basic Customer";
makeTransaction():string {
var transaction_id = Math.random().toString(36).substr(2, 9);
console.log(this.constructor.toString().split ('(' || /s+/)[0].split (' ' || /s+/)[1]);
return transaction_id;
}
constructor(public name:string, public dob:string) {
this.id = Math.random().toString(36).substr(2, 9);
}
}
This class extends customers to create a VIP customer:
class VIPCustomer extends Customer{
vip_num:string;
vip_discount:number;
static c_type = "VIP Customer";
constructor(public name:string, public dob:string) {
super(name, dob);
this.vip_num = Math.random().toString(36).substr(2, 9);
}
}
The customer creator is intended to create either a VIP customer or regular customer based on a string comparison, but it is not working.
class CustomerCreator {
static create(event: {name:string; dob: string}, type:string) {
console.log('Log type' + typeof type);
if (type === 'Basic') {
console.log('basic customer created');
return new Customer(event.name, event.dob);
}
if (type === 'VIP') {
console.log('basic customer created');
return new VIPCustomer(event.name, event.dob);
}
}
}
console.log(Customer.c_type);
console.log(VIPCustomer.c_type);
const customer_1 = CustomerCreator.create({name:'Pii', dob:'03/19'}, 'VIP');
var customer_2 = CustomerCreator.create({name:'Matthew', dob:'12/70'}, 'Basic');
//accessing an attribute
console.log(customer_1.name);
console.log(customer_1.id);
//console.log(customer_1.vip_num)
If you uncomment the last print statement, the code does not compile. The print statements also indicate that a basic customer is being created for both the customers 1 and 2, despite the string comparison. Where am I going wrong?
Typescript only has type info of compiling time, but not type info only known in run time.
The return type of CustomerCreator.create is Customer|VIPCustomer which is narrowed down to Customer so everything return from that function is recognized to ts compiler as Customer . That's the whole point of Factory pattern, that your code rely on interface but not class
If you really want to let compiler know what exact type of what CustomerCreator.create returns, you could try following code
type CreatorResult = {
Basic: Customer,
VIP: VIPCustomer
}
class CustomerCreator {
static create<T extends 'Basic'| 'VIP'>(event: {name:string; dob: string}, type:T): CreatorResult[T] {
although this is not recommended
Your solution is not working because the create factory method always returns the type Customer as VIPCustomer is also derived from Customer. Also, your create function not only returns just Customer but Customer | undefined because you do not have a default case (when type is neither Basic or VIP). I would just create multiple factory methods for each type of customer. In this case of my example there is almost no shared piece of code or extra processing, the factory pattern is rendered useless.
class CustomerCreator {
static create(event: { name: string; dob: string }) {
return new Customer(event.name, event.dob);
}
static createVip(event: { name: string; dob: string }) {
return new VIPCustomer(event.name, event.dob);
}
}
console.log(Customer.c_type);
console.log(VIPCustomer.c_type);
const customer_1 = CustomerCreator.createVip({ name: 'Pii', dob: '03/19' });
var customer_2 = CustomerCreator.create({ name: 'Matthew', dob: '12/70' });
console.log(customer_1.name);
console.log(customer_1.id);
console.log(customer_1.vip_num)
I want to add an object which has dynamic property name into an array but I don't know how to define the array
class Driver {
public id: string;
public name: string;
constructor(id , name) {
this.id = id;
this.name = name;
}
}
let drivers: Driver[];
let driver = new Driver("1111","tom");
drivers.push({[driver.id]:driver})
Since you don't know what the key for those objects will be beforehand, you'll want to use an index signature. So for your example, you would define the type like so:
let drivers: {[id: string]: Driver}[];
This question already has answers here:
How to access the correct `this` inside a callback
(13 answers)
Closed 5 years ago.
I am trying to push values dynamically to my data member inside the constructor,
but inside the forEach block, I'm not able to access that member.
Here AppComponentClass
export class AvailableRidersComponent {
public availableDrones: Pickdrone[];
constructor() {
const db = firebase.database();
const availableRidersRef = db.ref('/riders/active_riders/available/');
availableRidersRef.on('value', snapshot => {
snapshot.forEach(function (childSnapshot) {
const riderProfileRef = db.ref('/riders/Pickdrones/' + childSnapshot.key);
riderProfileRef.on('value', dataSnap => {
const riderProfile = {
name: dataSnap.val().name,
id: dataSnap.key,
contact: dataSnap.val().contact,
email: dataSnap.val().email
};
console.log(riderProfile); //Prints values properly
this.availableDrones.push(riderProfile); //ERROR TypeError: Cannot read property 'availableDrones' of undefined
});
});
});
}
}
and here us my Pickdrone class
export class Pickdrone {
public name: string;
public id: string;
public contact: string;
public email: string;
// vehicleType: string;
constructor(name: string, id: string, contact: string, email: string){
this.name = name;
this.contact = contact;
this.id = id;
this.email = email;
}
}
you have to initialize your availableDrones array , like this for example:
public availableDrones: Pickdrone[] = {};
and also for better code, make the constant riderProfile of type Pickdrone
as:
const riderProfile: Pickdrone = {
name: dataSnap.val().name,
id: dataSnap.key,
contact: dataSnap.val().contact,
email: dataSnap.val().email
};
so that the compiler can check if you input the correct type for each property.