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(...))
Related
I have a general interface which I use to define properties (let's say it's an Animal).
Then I have extensions of this interface, which have additional different fields.
I want to define a function that gets an instance of an animal, spreads it - and parses a specific field.
The result value should be the same as the input's; however I get an error, as not all animals are equal :P
How can I use the spread operator and still return the same type?
interface AnimalProps { name: string; size: string; }
class Dog implements AnimalProps {
name: string;
size: string;
bark(): void { console.log(`${this.name} says: 'woof'`); }
constructor(name: string, size: string) {
this.name = name;
this.size = size;
}
}
class Bird implements AnimalProps {
name: string;
size: string;
fly(): void { console.log(`${this.name} says: 'I love flying'`); }
constructor(name: string, size: string) {
this.name = name;
this.size = size;
}
}
type Animal = Dog | Bird;
function addSizeUnits(animal: Animal): Animal {
// Property 'fly' is missing in type '{ size: string; name: string; }' but required in type 'Bird'.
return { ...animal, size: `${animal.size} meters` };
}
Obviously I'm simplifying the existing (legacy) code here, but the bottom line is that I do need to use fly and bark and therefore I need the actual union types - and not the basic Animal.
A generic function should work here. Something like:
function addSizeUnits<T extends Animal>(animal: T) {
return { ...animal, size: `${animal.size} meters` };
}
Note that I removed the return type annotation because the type inference should figure that out for you, but if you want to add it back you can use something like T & { size: string } correction, just T should be fine because you already have size in AnimalProps.
This can easily be addressed by using a generic constraint:
function addSizeUnits<A extends Animal>(animal: A): A {
return { ...animal, size: `${animal.size} meters` };
}
This ensures that your argument type and your return type are the same type of Animal.
let's assume we have the following situation:
class BaseClass {
// ....some logic
}
class A extends BaseClass{
name: string;
constructor(name: string) {
this.name = name
}
someMethodFromInside() {
// ....some logic
}
}
class B extends BaseClass {
name: string;
constructor(name: string) {
this.name = name
}
someMethodFromInside() {
// ....some logic
}
}
And then i would like to choose which object of a class i'm creating, based on some string value, that i'm getting from outside:
const someStringFromOutside = 'key1'
const obj: IHandler = {
'key1': A,
'key2': B
}
try {
// here i would like to create object based on value in string choose to pick up class and create
// object
new obj[someStringFromOutside]('somevariable').someMethodFromInside()
} catch(err) {
...some error handling
}
Because i'm writing in TypeScript i also have to know how to properly type interface in handler:
I was trying to pass some kind ObjectConstructor for value in interface. But it was to generic and typescript display me error.
enum HandlerEvents {
KEY_1 = 'key1',
KEY_2 = 'key2'
}
interface IHandler {
[key in HandlerEvents]: .....what should be here ?
}
How do I add value to object in an array of objects in Typescript?
Say I have an array of object like this:
[{name:'John',age:20}, {name:'Sam',age:26}, {name:'Will',age:2}]
What I want is dynamically generated property in each object of array as this:
[{name:'John',age:20, value:'John 20'}, {name:'Sam',age:26, value:'Sam 26' }, {name:'Will',age:2, value:'Will 2'}]
This is what I tried: I have a created a model for type checking and created a method in it which will return the value.
export class Names {
name: string;
age: number;
constructor() {
}
getConcat() {
return this.name + ' ' + this.age;
}
}
Although when I get data from server of type Names[], I am not able to fetch the dynamically generated value. I am trying to get values like this:
this.somevariable:Names[]=[]; //declarartion
this.somevariable[0].getConcat() // inside a method after api call
How do I do that? Any help would be much appreciated. Thanks!
as you made the class for the purpose of typechecking - I'm guessing you mixed up javascript's class and typescript's interface:
an interface is only present during development, and gone in your actual production app.
a class is a living js object that is compiled into a functioning object in your app.
an interface for typechecking may look like this:
interface Name {
name: string;
age: number;
getConcat: () => string; // hints about the presence of a function that returns a string, within the typed object - but is not a function by itself!
}
while a class requires a bit modification
export class Names {
name: string;
age: number;
constructor(name: string, age: number) { // pass arguments to initialize the class object
this.name = name;
this.age = age;
}
getConcat() {
return this.name + ' ' + this.age;
}
}
using interfaces, you could say that the array of names you have is of type Name[], meaning:
function getConcat () {
return this.name + ' ' + this.age
}
const names: Name[] = [{name: 'a', age: 1, getConcat: getConcat }, {name: 'b', age: 2, getConcat: getConcat },...];
and throughout your code development, you will be hinted about the already preset getConcat method, and be warned if it is not there now, or in future.
But in order to use the class for both typechecking AND the getConcat function, you will have to map each object into a class:
const objects = [{name: 'a', age: 1}, {name: 'b', age: 2}];
const names: Name[] = objects.map(obj => new Name(obj.name, obj.age));
And now you can access any method you declared within Name class, for each item in names array.
You can actually mix the two up, initializing the js classes as shown above, while typechecking the class itself:
export class Name implements NameInterface {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getConcat() {
return this.name + ' ' + this.age;
}
}
And now you will be warned if your class object has not implemented any member of the NameInterface declared members (say you forgot to add the getConcat function within the class).
You want to insert new attribute then you should write like this: arr[i].value = arr[i].name + ' '+ arr[i].age;. Once you get data from API store it in an array and then do as shown below to update your array.
Following is working example:
arr = [{name:'John',age:20}, {name:'Sam',age:26}, {name:'Will',age:2}];
for(let i=0;i<3;i++){
arr[i].value = arr[i].name + ' '+ arr[i].age;
}
console.log(arr);
arr = [{name:'John',age:20}, {name:'Sam',age:26}, {name:'Will',age:2}];
const generateValue = (items) => {
return items.map(x=> {
return {...x, value: `${x.name} ${x.age}`};
});
};
arr = generateValue(arr);
console.log(arr);
You can create function like this
I want to do something that's probably unorthodox (and borderline useless if we're being honest) so here we go:
I want to pass a literal as a Generic parameter and then instantiate it. Consider the following example:
const log = console.log;
class Root<T = {}> {
// public y: T = {}; // this obviously doesn't work
// again this won't work because T is used a value. Even if it worked,
// we want to pass a literal
// public y: T = new T();
public x: T;
constructor(x: T) {
this.x = x;
}
}
class Child extends Root<{
name: "George",
surname: "Typescript",
age: 5
}> {
constructor() {
// Duplicate code. How can I avoid this?
super({
name: "George",
surname: "Typescript",
age: 5
});
}
foo() {
// autocomplete on x works because we gave the type as Generic parameter
log(`${this.x.name} ${this.x.surname} ${this.x.age}`);
}
}
const main = () => {
const m: Child = new Child();
m.foo();
};
main();
This works but I have to pass the literal twice. Once on generic for autocompletion to work and once on constructor for initialization. Ugh.
One other way to do it would be to declare my literal outside of Child. Like this:
const log = console.log;
class Root<T = {}> {
// public y: T = {}; // this obviously doesn't work
// again this won't work because T is used a value. Even if it worked,
// we want to pass a literal
// public y: T = new T();
public x: T;
constructor(x: T) {
this.x = x;
}
}
// works but ugh..... I don't like it. I don't want to declare things outside of my class
const literal = {
name: "George",
surname: "Typescript",
age: 5
}
class Child extends Root<typeof literal> {
constructor() {
super(literal);
}
foo() {
// autocomplete on x works because we gave the type as Generic parameter
log(`${this.x.name} ${this.x.surname} ${this.x.age}`);
}
}
const main = () => {
const m: Child = new Child();
m.foo();
};
main();
Is there any magical way to instantiate the Generic type without explicitly providing it again through a constructor?
You could use an intermediate wrapper that would take care of both expanding the generic and calling the constructor:
function fromRoot<T>(x: T) {
return class extends Root<T> {
constructor() {
super(x)
}
}
}
and then:
class Child extends fromRoot({
name: "George",
surname: "Typescript",
age: 5
}) { etc }
PG
You need to be aware that compiled Javascript doesn't know about generics and therefore you can't use them to create new object.
Also, I don't see the point of Child class if it'll be constrained to specific object - why don't you define type that your Child expects, and then instantiate child with specific instance of that type?
type MyType = {
name: string
surname: string
age: number
}
class Child extends Root<MyType> {
foo() {
// autocomplete on x works because we gave the type as Generic parameter
console.log(`${this.x.name} ${this.x.surname} ${this.x.age}`);
}
}
const child = new Child({
name: "George",
surname: "Typescript",
age: 5
})
If you want to reuse that specific Child you could just export that specific child instance.
Please see playground.
This is a typescript class with an exposed but not editable attribute:
export class Category {
private _id: number;
constructor(id: number){
this._id = id;
}
public get id(){
return this._id;
}
}
I would like to map it from a JSON like this:
{ id: 1 }
There are some obvious problems here:
I know that "_id" can't be magically mapped to "id" so maybe I could implement a custom logic that renames all the attributes with a _ before the name
I would like to keep the constructor with the id param but maybe I'm not able to map an object who require arguments before instantiation
With an empty constructor, I tried Object.assign(new Category(), jsonObject), however, this does not work since Cannot set property id of #<Category> which has only a getter
What I want to avoid is to write custom mapping logic for every class like this with private attributes, I tried some other solutions and libraries but didn't work for my situation, they're all referencing to class with only public attributes
I don't even know if what I ask is achievable, so if the case it isn't, then I will "surrender" to use the class with only public attributes
The missconception here is that you need a getter/setter at all just to manage visibility. You can't prevent code from accessing and modifying id, no matter what you do. You can however tell the IDE (and the developer using it) that he can only read/get the property by using the readonly modifier, which simplifies your code to:
export class Category {
readonly id: number;
}
Noe that readonly thing only exists at compile time, and doesnt have any affects at runtime. Your IDE will prevent you from doing:
(new Category).id = 5;
But it allows you to easily do:
const category = Object.assign(new Category, { id: 5 });
Pass the object through constructor:
export class Category {
private _a: number;
private _b: string;
constructor(values: { a: number; b: string }){
this._a = values.a;
this._b = values.b;
}
public getA() { return this._a; }
public getB() { return this._b; }
}
You can still call it with or without JSON:
let cat1 = Category (values: { a: 42, b: 'hello' });
let json = '{"a":42,"b":"hello"}'
let cat2 = Category (values: JSON.parse (json));
Alternatively, keep the values in an object rather than a direct member. This makes it unnecessary to map the object to member variables:
export class Category {
private _values: {
a: number;
b: string;
}
constructor(values: { a: number; b: string }){
this._values = values;
}
public getA() { return this._values.a; }
public getB() { return this._values.b; }
}
If you want to keep the old constructor, you can do so like this:
export class Category {
private _values: {
a: number;
b: string;
}
constructor(a: number, b: string) {
this._values = { a: a, b: b };
}
public static make (values: { a: number; b: string }) {
this._values = values;
}
public getA() { return this._values.a; }
public getB() { return this._values.b; }
}