Let classes inherit from type aliases - javascript

I try to create a user class and want to be able to inherit from a type alias:
type PlainUser = { email: string }
class User extends PlainUser {
constructor (initialValues: PlainUser) {
this.email = initialValues.email
}
update () { ... }
}
This doesn't work of course, but I would like to have the following semantics without having to duplicate email (and all the other fields that I don't show to keep it brief):
type PlainUser = { email: string }
class User {
email: string
constructor (initialValues: PlainUser) {
this.email = initialValues.email
}
update () { ... }
}
Is this possible with flow?

Not that I know of, but you can at least use implements to enforce that the User class implements the PlainUser interface (yes, you have to change it to be an interface).
interface PlainUser {
email: string;
}
class Foo implements PlainUser {
}
(tryflow)
The code above yields the following error with Flow v0.41, since Foo does not specify an email property:
7: class Foo implements PlainUser {
^ property `email` of PlainUser. Property not found in
7: class Foo implements PlainUser {
^ Foo
Of course, this isn't exactly what you've asked for. But at least you are getting automatic checking that User implements PlainUser, rather than nothing at all.

You can only extend from classes, and your type alias is an interface, so you have to use implement here. TypeScript Salsa allows doing the following since this suggestion was implemented:
type PlainUser = { email: string };
class User implements PlainUser {
constructor (initialValues: PlainUser) {
this.email = initialValues.email;
}
}
If you do not use salsa, you have to explicitly declare the inherited properties:
type PlainUser = { email: string };
class User implements PlainUser {
public email: string;
constructor (initialValues: PlainUser) {
this.email = initialValues.email;
}
}
Playground

I'll admit this was a head scratcher initially, but something like what you want to do is very possible. It does require rethinking the approach a bit.
First, you need to start with the class instead of the object literal. Intuitively this makes sense, as that's also the way javascript works.
class User {
email: string;
}
Next you want to use flow's $Shape transformation. This will cast your type to the enumerable properties of the class.
type PlainUser = $Shape<User>;
const Bob: PlainUser = { email: "bob#bob.com" }
or
const BobProperties: PlainUser = { ...new PlainUserClass("bob#bob.com") }
Finally, extend the User class as normal.
class AdminUser extends User {
admin: true;
}
example

Related

Using Typescript interface as a variable

I am working on a Node Js (TypeScript) architecture and for some reason, I want to bind my interface to a specific object. I am making a general class that is extended by other subclasses and it will have a very general code. So my code looks like
interface User {
name: string;
}
interface Profile {
title: string;
}
class Parent {
name: string;
interface: Interface; // Help required here, getting error can't use type as a variable
constructor( name, interface ) {
// Load schema and store here
this.name = name
this.interface = interface
}
// Though this is not correct I hope you get the idea of what I am trying to do
get (): this.interface {
// fetch the data and return
return data
}
set (data: this.interface): void {
// adding new data
}
}
class UserSchema extends Parent {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent {
// Class with custom functions for ProfileSchema
}
// Config file that saves the configs for different modules
const moduleConfig = [
{
name: "User Module",
class: UserSchema,
interface: User
},
{
name: "Profile Module",
class: ProfileSchema,
interface: Profile
},
]
const allModules = {}
// Loading the modules
moduleConfig.map(config => {
allModules[config.name] = new config.class(
config.name,
config.interface
)
})
export allModules;
I need suggestions on how should I bind my interfaces with their respective configs. Till now I have had no luck with that.
PS: All this code is separated into their respective files.
This is the use case for generics. You can even see them as "variable for types".
Instead of having an interface property in your Parent class, the latter would have a generic type:
class Parent<T> { // T is the generic type
name: string;
// interface: Interface; // generic is already provided at class level
constructor( name ) {
// Load schema and store here
this.name = name
}
get (): T {
// fetch the data and return
return data
}
set (data: T): void {
// adding new data
}
}
// Here you specify the concrete generic type
class UserSchema extends Parent<User> {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent<Profile> {
// Class with custom functions for ProfileSchema
}

Decorators and Private fields javascript

I find myself trying to use decorators with the native javascript private properties (#) and these first 'recognize' that are in use do not work.
I identified this by using the class-validator decorators on the private properties of my value objects.
The error I get in my code editor is: Decorators are not valid here
Example:
import { IsString } from 'class-validator';
Class Person {
#IsString()
#name: string;
constructor(name: string) {
this.#name = name;
}
get name(): string {
return this.#name;
}
}
Okey as suggested by VLAZ:
Private fields in JS are completely private and inaccessible to anything from outside. Thus it makes sense they cannot be decorated - there is no way for the decorator to access them.
This is completely correct, so when I took a closer look at the value object I realized that it does have public get properties, so by testing it is possible to use decorators on those properties.
Leaving something like:
import { IsString } from 'class-validator';
Class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
#IsString()
get name(): string {
return this.#name;
}
}

Typescript: Extend class with Partial<Type> constructor

Using typescript, how do extend the User class using Partial<User> as the constructor?
I am also open to solutions which do not use Partial. In this case I am only using the utility type to initialize a blank class. i.e. new User({})
Currently, AdvancedUser only has User properties, but none of the additional advanced?: properties.
export class User {
first_name: string = ''
last_name: string = ''
email: string = ''
constructor(data: Partial<User>) {
Object.assign(this, data)
}
}
export class AdvancedUser extends User {
advanced?: {
foo?: string
}
constructor(data: Partial<User>) {
super(data)
}
}
The provide code actually works. My project was suffering from a downstream typo reverting my AdvancedUser() call back to User().
I am also open to solutions which do not use Partial. In this case I am only using the utility type to initialize a blank class. i.e. new User({})
Instead of having constructors that use Partial, you can get the result you want by using the as keyword, which in my opinion is much cleaner.
As for the advanced property, the reason it's not showing up is because it isn't initialized anywhere (neither inline or in the constructor). Assuming you want to keep it as an optional property, all you need to do is initialize it with undefined:
export class User {
first_name: string = '';
last_name: string = '';
email: string = '';
constructor(data: User) {
Object.assign(this, data);
}
}
export class AdvancedUser extends User {
advanced?: {
foo?: string
} = undefined;
constructor(data: User) {
super(data);
}
}
const au = new AdvancedUser({} as User);
/* OUTPUT:
AdvancedUser: {
"first_name": "",
"last_name": "",
"email": "",
"advanced": undefined
}
*/
console.log(au);
How about something like this for using Partial?:
interface IUserData {
first_name: string;
last_name: string;
email: string;
}
interface IAdvancedUserData {
doAdvancedStuff(IAdvancedStuffOptions) : string;
}
class AdvancedUserData implements IUserData, IAdvancedUserData {
}
your User still accepts data of type Partial, then you pass an AdvancedUserData to your AdvancedUser constructor.

Mongoose loadClass issue with TypeScript

I'm taking advantage of mongoose class schemas.
And using TypeScript for my Node project.
I've followed Mongoose the Typescript way...? to make sure my Model is aware of the schema I've defined, So I've auto-completion etc..
However it becomes more tricky with schema class.
As written in their docs:
The loadClass() function lets you pull in methods, statics, and
virtuals from an ES6 class. A class method maps to a schema method, a
static method maps to a schema static, and getters/setters map to
virtuals.
So my code looks something like:
interface IUser extends mongoose.Document {
firstName: string,
lastName: string
};
const userSchema = new mongoose.Schema({
firstName: {type:String, required: true},
lastName: {type:String, required: true},
});
class UserClass{
static allUsersStartingWithLetter(letter: string){
return this.find({...});
}
fullName(this: IUser){
return `${this.firstName} ${this.lastName}`
}
}
userSchema.loadClass(UserClass);
const User = mongoose.model<IUser>('User', userSchema);
export default User;
My goal is that TypeScript will understand that:
User has a method allUsersStartingWithLetter
User instance has a method fullName
In the current configuration it does not.
I was not able to accomplish it myself.
Have you considered adding extends mongoose.Model to the UserClass?
class UserClass extends mongoose.Model<IUser> {
static allUsersStartingWithLetter(letter: string){
return this.find({...});
}
fullName(this: IUser){
return `${this.firstName} ${this.lastName}`
}
}
Do you really need to use classes? You could accomplish this using interfaces without using classes to do it. Here's an example:
/* eslint-disable func-names */
import mongoose from 'mongoose';
export interface Foo {
id?: string;
name: string;
createdAt?: Date;
updatedAt?: Date;
}
export type FooDocument = mongoose.Document & Foo;
const fooSchema = new mongoose.Schema(
{
name: { type: String, required: true },
},
{ timestamps: true }
);
fooSchema.methods.bar = function (): void {
const foo = this as FooDocument;
foo.name = 'bar';
};
const FooModel = mongoose.model<FooDocument>('foos', fooSchema);
export default FooModel;
This way you can use the Foo interface for methods with the inversion depedency. Them in your repository will return Foo instead of FooDocument...
Extra: If you use lean() in your database requests you return exactly the Foo interface. More information for lean here
Your UserClass needs to extend Model from mongoose. You seem to be missing a bit of required code to make this work for you.
From the link you shared as a reference, here's a guide that should solve your issue with complete code example.
https://stackoverflow.com/a/58107396/1919397

Using Typescript to force generic constraint for interface?

I have 2 interface declarations :
interface IStore { }
interface SomethingElse { a: number;}
And 2 classes which implements each:
class AppStoreImplemetion implements IStore
{ }
class SomethingImplementation implements SomethingElse
{
a: 4;
}
I want my method to be given the return type as a constraint of "must be IStore" , so I did this:
class Foo {
selectSync<T extends IStore>( ): T
{
return <T>{/* omitted*/ }; // I set the return type(`T`) when invoking
}
}
OK
Testing :
This works as expected :
new Foo().selectSync<AppStoreImplemetion>();
But this also works - not as expected :
new Foo().selectSync<SomethingImplementation>();
Question:
How can I force my method to accept a return type which must implement IStore ?
Online demo
The problem is Typescript usses structural typing to determine type compatibility, so the interface IStore which is empty, is compatible with any other type, including SomethingElse
The only way to simulate nominal typing (the kind you have in C#/Java etc.) is to add a field that makes the interface incompatible with other interfaces. You don't actually have to use the field, you just have to declare it to ensure incompatibility:
interface IStore {
__isStore: true // Field to ensure incompatibility
}
interface SomethingElse { a: number; }
class AppStoreImplemetion implements IStore {
__isStore!: true // not used, not assigned just there to implement IStore
}
class SomethingImplementation implements SomethingElse {
a = 4;
}
class Foo {
selectSync<T extends IStore>(): T {
return <T>{/* omitted*/ };
}
}
new Foo().selectSync<AppStoreImplemetion>();
new Foo().selectSync<SomethingImplementation>(); // This will be an error
Note that any class that has __isStore will be compatible regardless of weather it explicitly implements IStore, again due to the fact that Typescript uses structure to determine compatibility, so this is valid:
class SomethingImplementation implements SomethingElse {
a = 4;
__isStore!: true
}
new Foo().selectSync<SomethingImplementation>(); // now ok
In practice IStore will probably have more methods, so such accidental compatibility should be rare enough.
Just as a side note, private fields ensure 100% incompatibility for unrelated classes, so if it is possible to make IStore an abstract class with a private field. This can ensure no other class is accidentally compatible:
abstract class IStore {
private __isStore!: true // Field to ensure incompatibility
}
interface SomethingElse { a: number; }
class AppStoreImplemetion extends IStore {
}
class Foo {
selectSync<T extends IStore>(): T {
return <T>{/* omitted*/ };
}
}
new Foo().selectSync<AppStoreImplemetion>(); // ok
class SomethingImplementation implements SomethingElse {
private __isStore!: true;
a = 10;
}
new Foo().selectSync<SomethingImplementation>(); // an error even though we have the same private since it does not extend IStore

Categories