Typescript - Extending class with type - javascript

Typescript newbie here.
Let's say I have a type coming from a library that looks like this:
type FooType {
name: string;
// Has much more attributes in real life
}
I now want to define a class called Foo like this:
import { FooType } from 'my-library';
class Foo {
constructor(data: FooType) {
Object.assign(this, data);
}
}
With this code, I'm able to define a foo instance, yet I have an issue with autocomplete:
const foo = new Foo({ name: 'foo name' });
// Typing in foo.name does not bring any type information about the "name" attribute
Is there a way I can make a class automatically "inherit" all attributes from a type without having to type them manually?
Edit after being marked as duplicate:
What I want to achieve is to avoid manually typing attributes that are already existing on a type.
Thanks to #Phil I've been provided an answer that mentions this as an ongoing issue within Typescript:
https://github.com/microsoft/TypeScript/issues/26792
What I will do for now is the following:
class Foo {
constructor(public _data: FooType)
Object.assign(this, _data);
}
get() {
return this._data;
}
}
const foo = new Foo({ name: 'Bar' });
foo.get().name; // After typing "foo.get()" the autocomplete works properly.

Note: this is just a workaround and doesn't fix the underlying issue with using Object.assign() in the constructor with TypeScript.
https://github.com/Microsoft/TypeScript/issues/16672
Update: this question definitely lead me down quite the rabbit and I wasn't quite happy with my previous response so here is something I think would work better for solving these two items:
Creating a class which extends an object
Allowing TypeScript to still do its thing
Step One: use the typescript mixin pattern to return an anonymous class with the properties of a generic object T:
function GenericBase<T>(data: T) {
return class {
constructor() {
Object.assign(this, data)
}
} as ({
new (...args: any[]): T;
})
}
Now we have an anonymous function with a constructor which is then casted as a class that returns our generic object T.
Step Two: Note that we don't need to touch the above code we just need to extend it, so for OP's example we would do this:
class MyClass extends GenericBase(foo) {
constructor() {
super()
}
greet() {
return `Hello, ${this.name}!`
}
}
More examples:
const myClass = new MyClass()
console.log(myClass.name) // bar
console.log(myClass.greet()) // Hello, bar!
function doSomethingWithFoo(someFoo: Foo) {
// example method that only takes types Foo
}
doSomethingWithFoo(myClass)
Try it on the TypeScript playground!

Related

Passing correct context with "this" to external method from class

Let's say I have a very simple class that extends other classes, and an object of functions i'm passing to class B
const actions = {
doSomething: (this: B, foo: boolean) => {
console.log('from do something', this.text, foo);
}
}
class B {
actions = actions;
text: string;
constructor() {
this.text = 'abc';
}
}
class A extends B {
runAction(foo) {
this.actions.doSomething(false);
}
}
const instance = new A();
instance.runAction(); // 'from do something, undefined, false'
ts compiler is also saying the this context isn't correct when calling from within runAction
Is there a better way to do this?
We have over 500 actions, and we want to provide access to the class data to every action and don't want to pass the arguments through all the actions.
This
this.actions.doSomething(false);
calls the method with a this of this.actions. But this.actions is the plain object with the doSomething method - not the instance itself. You will need to use .call instead, as well as changing the doSomething arrow function to a regular function/method so it can capture a different this. The foo argument should be typed too (or removed).
Typing this inside runAction could be useful as well, though not always necessary.
const actions = {
doSomething(this: B, foo: boolean) {
console.log('from do something', this.text, foo);
}
}
class A extends B {
runAction(this: A) {
this.actions.doSomething.call(this, false);
}
}

Question concerning types, Maps and subclasses in typescript

I have a Chart class which has several subclasses (BarChart, TimeseriesChart...) which extend Chart.
I use a method called buildChart to build these charts. It maps the enum ChartsEnum (for example stackedTimeseries or barChart) to the correct class using a Map:
export function buildChart(type: Charts, data: Array<TimeseriesData>) {
var chartsMap = new Map<Charts, InstantiableAbstractClass<typeof Chart>([
[Charts.stackedTimeseries, TimeseriesChart],
[Charts.barChart, BarChart],
])
const chart = chartsMap.get(type)
return new chart(data).chart;
}
The type InstantiableAbstractClass looks like this:
export declare type InstantiableClass<T> = (new ( ...args: any) => { [x: string]: any }) & T;
For example, the class and constructor of TimeseriesChart looks like this:
export class TimeseriesChart extends Chart{
constructor(data: Array<TimeseriesData>) {
super(data);
}
}
I now want to add a second attribute to the chart-class called options (next to the existing attribute data).
The problem is now that options requires for each ChartType (BarChart, TimeseriesChart) different properties.
For example BarChart requires these properties:
{
start: number;
end?: number;
}
and TimeseriesChart requires a type like this:
{
description: string;
}
The constructor of TimeseriesChart would then look like this:
export class TimeseriesChart extends Chart{
constructor(data: Array<TimeseriesData>, options: TimeseriesChartOptions) {
super(data, options);
}
}
This means that the method buildChart needs a new argument options which can then be passed to the specific classes (as done with the argument data).
What is the best way for doing this? I thought of using generics and then defining n types of types for options for n subclasses, but I could not figure out on how to change the type InstantiableAbstractClass correctly for that.
You can find a complete example with some descriptions here.
I really appreciate your help! If you need any further information I would be happy to provide them.
Thank you and all the best
Lukas
Firstly, it seems like you want your Chart class to be abstract since you are only ever going to use concrete subclasses of it, and you want the buildChart() method to throw if not overridden. I'm also going to use parameter properties as a shorthand for declaring a field and an idential constructor parameter and assigning the latter to the former. I'm adding an options parameter of the any type because we don't really need any type checking in the base class like this.
abstract class Chart {
constructor(protected data: Array<TimeseriesData>, protected options: any) { }
abstract buildChart(): void;
}
Anyway, for the subclasses, we will narrow the options property to the appropriate type by using the declare property modifier, and specify that type in the constructor parameter also:
interface BarChartOptions {
start: number,
end: number
}
class BarChart extends Chart {
declare options: BarChartOptions;
constructor(data: Array<TimeseriesData>, options: BarChartOptions) {
super(data, options);
}
buildChart() { return {}; }
protected getChartData() { }
}
interface TimeseriesChartOptions {
description: string
}
class TimeseriesChart extends Chart {
declare options: TimeseriesChartOptions;
constructor(data: Array<TimeseriesData>, options: TimeseriesChartOptions) {
super(data, options);
}
buildChart() { return { }; }
protected getChartData() { }
}
Now we need to express the relationship between your Charts enum and the different subclasses. Let's make a chartConstructors object holding the enums as keys and the constructors as values:
const chartConstructors = {
[Charts.stackedTimeseries]: TimeseriesChart,
[Charts.barChart]: BarChart
}
You should add an entry to that for each subclass of Chart you care about.
Finally we will write your buildChart() function. Let's make some helper types:
type ChartConstructorParameters<C extends Charts> =
ConstructorParameters<typeof chartConstructors[C]>;
type ChartInstance<C extends Charts> =
InstanceType<typeof chartConstructors[C]>;
type ChartConstructor<C extends Charts> =
new (...args: ChartConstructorParameters<C>) => ChartInstance<C>;
The ChartConstructorParameters<C> type will take an enum member as C and turn it into the tuple of arguments to the relevant class constructor, using the ConstructorParameters<T> utility type.
The ChartInstance<C> type does the same thing for the instance type of the relevant class constructor, using the InstanceType<T> utility type.
And finally the ChartConstructor<C> represents the relevant constructor signature for the class.
And now here's buildChart():
function buildChart<C extends Charts>(
type: C, ...ctorArgs: ChartConstructorParameters<C>
) {
const chartConstructor = chartConstructors[type] as ChartConstructor<C>;
return new chartConstructor(...ctorArgs);
}
It's a generic function which takes a type parameter of generic type C corresponding to an enum member. And for the remaining arguments, it takes whatever the constructor parameters of the relevant class is. This will be a pair of data and options for the BarChart and TimeseriesChart, but if you add subclasses with other constructor parameters, it will accept those.
The output is an instance of the class, which it constructs by grabbing the relevant constructor from chartConstructors (we need to use a type assertion to convince the compiler that chartConstructors[type] is actually of the type ChartConstructor<C>; it's something the compiler is unable to see due to its inability to reason too much about unspecified generic types like C).
So, does it work?
const barChart = buildChart(Charts.barChart, [], { start: 1, end: 2 });
// const barChart: BarChart
const timeSeriesChart = buildChart(Charts.stackedTimeseries, [], { description: "" });
// const timeSeriesChart: TimeseriesChart
Yes, looks good!
Playground link to code

Parse.com with TypeScript: Extend Parse.Object

Disclaimer: I know, Parse.com shuts down it's hosted service. Still, we will continue to use the framework for a while, so this question is still important to us.
Recently, I started playing around with TypeScript and figured it might enhance my productivity for parse cloud code a lot. So I did some testing and was successfully able to use typescript to write cloud functions and so on. I even included the typing definition for parse via typings.
However, I still don't get one thing: How can I extend Parse.Object in a type-safe manner?
In normal js I would write:
var Foo = Parse.Object.extend("Foo", {
// instance methods
}, {
// static members
});
In order to get type safety, I would like to write something like this in typescript:
class Foo extends Parse.Object {
// static members
// instance methods
}
Is something like this possible? Am I missing out on something?
Yes, this is possible. There are a few steps necessary for this. First, you need to pass the classname to the parent constructor
class Foo extends Parse.Object {
// static members
// instance methods
constructor() {
// Pass the ClassName to the Parse.Object constructor
super('Foo');
}
}
Furthermore, you need to register your class as an Parse object:
Parse.Object.registerSubclass('Foo', Foo);
After this, you can just use it for your query as:
var query = new Parse.Query(Foo);
query.find({
success: obj: Foo[] => {
// handle success case
},
error: error => {
// handle error
}
});
This also works for the new parse server open source project: https://github.com/ParsePlatform/parse-server
If someone like my stumble across the same question, here are my findings so far:
import Parse from "parse";
interface IBase {
readonly createdAt?: Date;
readonly updatedAt?: Date;
}
interface IMyInterface extends IBase {
name: string;
age?: number;
}
class MyClass extends Parse.Object<IMyInterface> {
constructor(attributes: IMyInterface) {
super("MyClass", attributes);
}
}
export default MyClass;
You can then use it like this:
const newObject = new MyClass({ name: theName, age: theAge });
const result = await newObject.save();
const { attributes } = result;
console.log("Save result", attributes);
You will have full TS support for the attributes then.
Hopefully the work on https://www.npmjs.com/package/#parse/react will proceed quickly.

How to reduce javascript object to only contain properties from interface

When using typescript a declared interface could look like this:
interface MyInterface {
test: string;
}
And an implementation with extra property could be like this:
class MyTest implements MyInterface {
test: string;
newTest: string;
}
Example (here the variable 'reduced' still contain the property 'newTest'):
var test: MyTest = {test: "hello", newTest: "world"}
var reduced: MyInterface = test; // something clever is needed
Question
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Why
The problem occur when trying to use the 'reduced' variable with angular.toJson before sending it to a rest service - the toJson method transforms the newTest variable, even if it's not accessible on the instance during compile, and this makes the rest service not accept the json since it has properties that shouldn't be there.
It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty
//this code transpiles to empty!
interface MyInterface {
test: string;
}
Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.
The answer by #jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -
class MyInterface {
test: string = undefined;
}
Then you can use lodash to pick the properties from the 'interface' to inject into you object:
import _ from 'lodash'; //npm i lodash
const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property
see JSFiddle
This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test
TS 2.1 has Object Spread and Rest, so it is possible now:
var my: MyTest = {test: "hello", newTest: "world"}
var { test, ...reduced } = my;
After that reduced will contain all properties except of "test".
Another possible approach:
As other answers have mentioned, you can't avoid doing something at runtime; TypeScript compiles to JavaScript, mostly by simply removing interface/type definitions, annotations, and assertions. The type system is erased, and your MyInterface is nowhere to be found in the runtime code that needs it.
So, you will need something like an array of keys you want to keep in your reduced object:
const myTestKeys = ["test"] as const;
By itself this is fragile, since if MyInterface is modified, your code might not notice. One possible way to make your code notice is to set up some type alias definitions that will cause a compiler error if myTestKeys doesn't match up with keyof MyInterface:
// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'
// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'
That's not very pretty, but if you change MyInterface, one or both of the above lines will give an error that hopefully is expressive enough that the developer can modify myTestKeys.
There are ways to make this more general, or possibly less intrusive, but almost no matter what you do, the best you can reasonably expect from TypeScript is that your code will give compiler warnings in the face of unexpected changes to an interface; not that your code will actually do different things at runtime.
Once you have the keys you care about you can write a pick() function that pulls just those properties out of an object:
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}
And them we can use it on your test object to get reduced:
var test: MyTest = { test: "hello", newTest: "world" }
const reduced: MyInterface = pick(test, ...myTestKeys);
console.log(JSON.stringify(reduced)); // {"test": "hello"}
That works!
Playground link to code
Are you trying to only set/assign properties listed on the interface only? Functionality like that is not available in TypeScript but it is very simple to write a function to perform the behaviour you looking for.
interface IPerson {
name: string;
}
class Person implements IPerson {
name: string = '';
}
class Staff implements IPerson {
name: string = '';
position: string = '';
}
var jimStaff: Staff = {
name: 'Jim',
position: 'Programmer'
};
var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);
function limitedAssign<T,S>(source: T, destination: S): void {
for (var prop in destination) {
if (source[prop] && destination.hasOwnProperty(prop)) {
destination[prop] = source[prop];
}
}
}
In your example newTest property won't be accessible thru the reduced variable, so that's the goal of using types. The typescript brings type checking, but it doesn't manipulates the object properties.
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Since TypeScript is structural this means that anything that contains the relevant information is Type Compatible and therefore assignable.
That said, TypeScript 1.6 will get a concept called freshness. This will make it easier to catch clear typos (note freshness only applies to object literals):
// ERROR : `newText` does not exist on `MyInterface`
var reduced: MyInterface = {test: "hello", newTest: "world"};
Easy example:
let all_animals = { cat: 'bob', dog: 'puka', fish: 'blup' };
const { cat, ...another_animals } = all_animals;
console.log(cat); // bob
One solution could be to use a class instead of an interface and use a factory method (a public static member function that returns a new object of it's type). The model is the only place where you know the allowed properties and it's the place where you don't forget to update them accidentaly on model changes.
class MyClass {
test: string;
public static from(myClass: MyClass): MyClass {
return {test: myClass.test};
}
}
Example:
class MyTest extends MyClass {
test: string;
newTest: string;
}
const myTest: MyTest = {test: 'foo', newTest: 'bar'};
const myClass: MyClass = MyClass.from(myTest);

Implementing prototypes for interfaces in TypeScript

I have created a TypeScript interface for my service results. Now I want to define a basic functionality for both my functions inside. The problem is I get an error:
The property 'ServiceResult' does not exist on value of type 'Support'.
I use WebStorm for development (VS2012 makes me nervous because on freezes by large projects - waiting for better integration:P).
Here's how I do it:
module Support {
export interface ServiceResult extends Object {
Error?: ServiceError;
Check?(): void;
GetErrorMessage?(): string;
}
}
Support.ServiceResult.prototype.Check = () => {
// (...)
};
Support.ServiceResult.prototype.GetErrorMessage = () => {
// (...)
};
I have also tried to move my prototypes into the module, but same error still... (of course I removed Support. prefix).
You can't prototype an interface because the compiled JavaScript does not emit anything related to the interface at all. The interface exists purely for compile-time use. Take a look at this:
This TypeScript:
interface IFoo {
getName();
}
class Foo implements IFoo {
getName() {
alert('foo!');
}
}
Compiles to this JavaScript:
var Foo = (function () {
function Foo() { }
Foo.prototype.getName = function () {
alert('foo!');
};
return Foo;
})();
There is no IFoo in the result, at all - which is why you are getting that error. Typically you wouldn't prototype an interface, you would prototype a class that implements your interface.
You don't even have to write the prototype yourself, just implementing the interface as a class is enough and the TypeScript compiler will add the prototype for you.
It looks like you are trying to add implementation to an interface - which isn't possible.
You can only add to a real implementation, for example a class. You may also decide to just add the implementation to the class definition rather than directly using prototype.
module Support {
export interface ServiceResult extends Object {
Error?: ServiceError;
Check?(): void;
GetErrorMessage?(): string;
}
export class ImplementationHere implements ServiceResult {
Check() {
}
GetErrorMessage() {
return '';
}
}
}
Support.ImplementationHere.prototype.Check = () => {
// (...)
};
Support.ImplementationHere.prototype.GetErrorMessage = () => {
// (...)
};

Categories