I am trying to understand interface topic in Typescript
when I came across Class type, I got this code from official docs
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
I can understand that Clock has no match for the signature new (hour: number, minute: number); that's why we get an error there.
But in docs the explaination is something which I am unable to understand. It goes in this way :
This is because when a class implements an interface, only the instance side of the class is checked. Since the constructor sits in the static side, it is not included in this check.
Any explanation would be appreciated.
The interface declares the method/members that the instances have, and not what the implementing class has.
For example check the Array and ArrayConstructor declarations:
interface Array<T> {
length: number;
toString(): string;
toLocaleString(): string;
push(...items: T[]): number;
pop(): T | undefined;
...
[n: number]: T;
}
interface ArrayConstructor {
new (arrayLength?: number): any[];
new <T>(arrayLength: number): T[];
new <T>(...items: T[]): T[];
(arrayLength?: number): any[];
<T>(arrayLength: number): T[];
<T>(...items: T[]): T[];
isArray(arg: any): arg is Array<any>;
readonly prototype: Array<any>;
}
As you can see, the Array has method/members which exist on any instance of array:
let a = [];
a.push(1, 2, 3);
console.log(a.length);
But the ArrayConstructor has the members/methods which exist on the Array itself:
console.log(Array. prototype);
console.log(Array.isArray(9));
The constructors are part of the "static" part which is why they are declared in the ArrayConstructor.
If you declare a constructor on an interface for example you'll have a problem implementing that interface:
interface MyInterface {
constructor();
getName(): string;
}
class MyClass implements MyInterface {
constructor() {}
getName() { return "name" };
}
Error:
Class 'MyClass' incorrectly implements interface 'MyInterface'. Types
of property 'constructor' are incompatible. Type 'Function' is not
assignable to type '() => void'. Type 'Function' provides no match for
the signature '(): any'.
Before you can get an instance you need to use the static side, the constructor, to get an instance. You don't need new in your interface anyway, your class is typed itself so typescript knows whatever arguments it has to pass along the constructor.
You can make benefits of an interface with type new if you want to pass a function or class that has to meet certain constructor requirements before it can be instantiated.
interface IFoo {
new(title: string);
}
function MyFunction(ctor: IFoo, title:string) {
return new ctor(title);
}
class MyClass {
constructor(public title: string) {}
}
class MySecondClass {
constructor(public title: string) {}
}
var myClass = MyFunction(MyClass, 'title');
var mySecondClass = MyFunction(MySecondClass, 'title');
console.log(myClass.title, mySecondClass.title);
In fact, a TypeScript class is a regular function in JavaScript which is static when you don't use new in front of it. This is were the docs are referring to.
// static side
function Person() {
}
Person.SayHi = function () {
return 'Hello';
}
console.log(Person.SayHi()); // static function..
var person = new Person() // instance side
See also this answer
Related
A good function is declared as follows:
export declare class SOMETHING implements OnDestroy {
sayHello() {
// some code to say hello
}
}
But in node_modules (angular material specifically) I found this function code in typesript:
export declare class SOMETHING implements OnDestroy {
sayHello(parA: string, parB?: string: parC: MatSnackBarConfig): MartSnackBarRef<SimpleSnackBar>;
}
But.... where is the {} in the function sayHello?
Where I can find information about this topic?
Thanks!
It's called a method declaration. You are stating to typescript that this method will be implemented and that it will be it's type.
It's useful in Interfaces and Abstract classes, and also method overload.
For example, the overload, in here I declare that the method findOneAndUpdate have two different way to be called, which leads to two different results.
public findOneAndUpdate<U = T>(data: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject: true;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<CollectionDocument<U>>;
public findOneAndUpdate<U = T>(data: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject?: false;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<U>;
In addition to the type declaration, of course you need to implement the method :
public findOneAndUpdate<U = T>({
where = {},
action = {},
option = {
new: true,
},
createObject = false,
session = false,
mustExist = false,
noLean = false,
schemaPosition,
}: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject?: boolean;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<CollectionDocument<U>> | Promise<U> {
// ...
}
This is closely related to abstract method deceleration - but more flexible without the abstract keyword. This way it is possible to expose the shape of the parent class in a more type conscious way.
class Parent {
hello(x: number): number;
}
class Child extends Parent {
hello() {
console.log('hello');
}
}
c = new Child();
c.hello()
Read here for more information:
https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes
Somewhere along the way, I added a constructor to my Todo class:
export class Todo {
id: number;
title: string;
complete: boolean = false;
editMode: boolean = false;
constructor(values: Object = {}) {
Object.assign(this, values);
}
}
I don't understand the purpose of the code in the constructor.
My application seems to work both with and without it, but I am hesitant to remove the code
What is the purpose of Object.assign(...) in this constructor?
This a method to easily add the values of the parameters of a class to their respective class fields where a class implements that interface or at least has a partial implantation of that interface.
interface IPerson {
firtName: string;
lastName: string;
}
class Person implements IPerson {
public firtName!: string;
public lastName!: string;
constructor(params: IPerson) {
Object.assign(this, params);
}
}
Your application works because you seem to have implemented this in such a way that the callback value of values to also be enough.
The main issue with this Hack is that Object.assign is not type safe. So using it in this way in a way goes against the point of TypeScript.
If you want to do this in a type safe fashion you are better off using a custom implementation where the type is properly checked. Something like this:
type PDM = PropertyDescriptorMap;
export class ClassSAssign<T> {
constructor(private objectToSpread: T, private klass: T) {}
private propertyDescriptorOptions = {
enumerable: true,
writable: true
};
public apply(): void {
const map = this.getPropertiesDescriptorMap();
Object.defineProperties(this.klass, map);
}
private getPropertiesDescriptorMap(): PDM {
return Object.entries(this.objectToSpread).reduce(
(obj: PDM, entry) => this.getPropertyDescriptorMap(obj, entry),
{}
);
}
private getPropertyDescriptorMap(obj: PDM, [key, value]: [string, any]): PDM {
return {
...obj,
[key]: {
value,
...this.propertyDescriptorOptions
}
};
}
}
and you can use this utility like this:
class Person implements IPerson {
public firtName!: string;
public lastName!: string;
constructor(params: IPerson) {
new ClassSAssign(params, this).apply();
}
}
If you don't/can't want to use the above, I suggest you at least add some type rigour to protect your class from what values can be passed into it
interface IToDo {
id?: number;
title?: string;
}
export class Todo implements IToDo {
public id?: number;
public title?: string;
public complete: boolean = false;
public editMode: boolean = false;
constructor(values?: IToDo) {
Object.assign(this, values);
}
}
Object.assign assigns all of the properties of the second argument to the first argument.
What the code does is if you pass an object into the constructor, it will assign those properties to the object that is being made. So for instance:
const todo = new Todo({ id: 1, title: 'hello' });
console.log(todo.title); // 'hello'
Edit:
Because Object.assign is not type-safe, you should probably have the constructor accept something more specific than just an Object. I would suggest creating an interface for it.
Object.assign has no type checking. An alternative would be:
const assign = <T, K extends keyof T>(...args: T[]): T =>
args.reduce( (result, current) =>
(Object.keys(current) as K[]).reduce((target, key) => {
target[key] = current[key];
return target;
}, result)
, args[0])
;
Note that if T's properties aren't optional, every object passed in must include every property. If you can guarantee the presence of every property after the function returns, you can pass in the arguments as Partial<T>, then coerce the result when you're done.
Its just combining the two objects this and values. According to MDN
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
Its used to create a shallow copy of the object and merge its properties with this which is the instance of Todo. In your given code this target object. Consider the below example
let target = {a:1,b:2};
Object.assign(target,{x:"new prop 1",y:"new prop 2"});
console.log(target)
Consider the following Typescript snippet:
class Animal {
constructor(name: string) {
this.name = name;
}
name: string;
haveBaby(name: string): ?? return type ?? {
return new this.constructor(name); // Error
}
}
class Cat extends Animal {}
class Dog extends Animal {}
class Gerbil extends Animal {} // etc.
let sorachi = new Cat("Sorachi"); // a: Cat
let sorachiJr = a.haveBaby("Sorachi Jr."); // I want: sorachiJr: Cat
Animals can have babies, and a baby should be the same kind of animal as the parent, i.e., should be an instance of the same class as the parent. How do I assign types in this situation, so that Typescript knows that sorachiJr: Cat?
The code snippet above doesn't work. The line return new this.constructor(name) produces the error [ts] Cannot use 'new' with an expression whose type lacks a call or construct signature. in VS Code. The only solution I was able to find and understand was replacing this.constructor(name) with (<any>this.constructor)(name) or (<any>this).constructor(name), but then the type inferred for sorachiJr is any, too, rather than Cat. I tried casting to typeof this rather than any, but got the error [ts] Cannot find name 'this'.
How can I convince Typescript that having babies is a species-preserving operation?
The preserving the type of the class the method was called on is easy, we just use the polymorphic this type. To convince the ts that constructor will be a constructor that takes a string and returns an instance the same type as the current class requires a type assertion
type AnimalConstructor<T extends Animal> = new (name: string) => T
class Animal {
constructor(name: string) {
this.name = name;
}
name: string;
haveBaby(name: string): this {
return new (this.constructor as AnimalConstructor<this>)(name);
}
}
class Cat extends Animal {}
class Dog extends Animal {}
class Gerbil extends Animal {} // etc.
let sorachi = new Cat("Sorachi"); // a: Cat
let sorachiJr = sorachi.haveBaby("Sorachi Jr.");
Note Typescript can't validate the fact that the derived class constructor only expects a single string parameter, it might require more or less parameters. This makes this constructor not type safe.
I have declared interface as below
interface Base {
required: string;
}
I have implemented interface in class like
class MyClass implements Base{
method(): void {
console.log(this.required);
}
}
But I am getting following error
severity: 'Error' message: 'Class 'MyClass' incorrectly implements
interface 'Base'. Property 'required' is missing in type 'MyClass'.'
at: '5,7' source: 'ts'
severity: 'Error' message: 'Property 'required' does not exist on type
'MyClass'.' at: '7,26' source: 'ts'
if I declare required: string; once again in class then no error
interface Base {
required: string;
}
class MyClass implements Base{
required: string;
method(): void {
this.required="ddd";
console.log(this.required);
// you can access HTMLElement
}
}
var ss=new MyClass();
ss.method();
If you don't want to delcare requried: string twice use class instate interface for Base and extends instate implements.
class Base {
required: string;
}
class MyClass extends Base{
method(): void {
this.required="ddd";
console.log(this.required);
// you can access HTMLElement
}
}
Check it out in the playground.
That's how interfaces work. If you define a property in the interface then you need to define the same property in the class where you are implementing the interface. If you would like to use required property without re define the property, you should create a class an extend it.
Your error is correct. If your class implements an interface, it must also implement all the required properties and methods. If you want not to implement some properties or methods, you can declare them as optional with ? symbol.
interface Base {
required: string;
someProperty?: string; // << look at the symbol `?` in the end of the property
}
Here you can implement the interface and left the someProperty
class MyClass implements Base{
required: string;
// someProperty is missing here, because it is optional
method(): void {
this.required="ddd";
console.log(this.required);
// you can access HTMLElement
}
}
And not only you can implement interfaces. Also you can use them as a type. If you have an interface
interface Base {
required: string;
}
you can create an object which is the type of that interface
const obj: Base = { };
But here you will get an error because if your object is of type Base, you need to provide all required properties. So you need to write
const obj: Base = { required: 'Yes' };
This will protect your code from logical errors and your code will be strong typed also for object, for which you don't want to create a class, but you want to said what shape it must be.
Example
You have an interface
interface Name {
name: string
}
and have classes
class Car implements Name {
name: string;
engine: string
constructor(name: string, engine: string){
this.name = name;
this.engine = engine;
}
}
class Person implements Name {
name: string;
surname: string;
constructor(name: string, surname: string){
this.name = name;
this.surname = surname;
}
}
var arr: Name = [new Car('Car', 'JZ'), new Person('Name', 'Surname')];
here arr is an array of type Name. So if you get arr[0], and call on it .engine, intelisense will throw an error that there is no engine property in type Name. But you can be sure, that every object in that array has name property, because the type of that array is Name and it has an required property name.
Just trying to learn TypeScript.
Not sure why I'm still getting below error.
Note: I have tried meeting all the requirements of interface
interface Greet {
greet(name?: Greet): string;
(val: string): string;
}
class Person implements Greet {
greet(name?: Greet): string {
return 'Hello ' + name;
}
obj(val: string):string {
return 'Hello';
};
}
Error
TsFiles/OopsTest.ts(8,7): error TS2420: Class 'Person' incorrectly implements interface 'Greet'.
Type 'Person' provides no match for the signature '(val: string): string'
8:26:50 PM - Compilation complete. Watching for file changes.
If you're trying to create a hybrid type then, per the documentation, the implementation should look something like:
function getPerson(): Greet {
let person = <Greet>function(val: string) { return '' };
person.greet = function(name?: Greet) { return '' };
return person;
}
Having (val: string): string; in the interface requires the implementation of Greet to be a function, not a class.
interface Greet {
greet(name?: Greet): string;
obj(val: string): string; // <<<=== `obj` missing
}