Javascript function without curly braces - javascript

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

Related

How to implement a Typescript interface that allows additional properties?

Ok, I have been struggling with this one as all information I find is about how to define interfaces that allow other properties, but not how to create a class that can implement the interface.
I have (or want to have) the following interface:
export interface IEnvironment {
value: string;
names: string[];
[x: string | 'value' | 'names']: (() => boolean) | string | string[]
};
Then I want a class that implements said interface, but I only want to implement the value and names properties.
For full disclosure, I want to create an environment object with value, names and one function per name in names. Like this:
export class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};
Now this works except for one error I get:
Class 'Environment' incorrectly implements interface 'IEnvironment'.
Index signature for type 'string' is missing in type 'Environment'.
So how can I do this? I'm a noob in the TypeScript arena, so I'd rather ask the experts.
If no solution, could this be worked around with another interface that extends IEnvironment? Like remove the extra properties thing and move it to another interface that I would use as consumer of the object so I get the correct Intellisense.
Thank you very much in advance.
You just need to declare this dynamic x property from interface as class property.
Add this line as your class property: [x: string]: string|(() => boolean)|string[];
Finally, your class looks like this:
class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
//ADD THIS
[x: string]: string|(() => boolean)|string[];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};

How to resolve Intersection and Union doesn't match in ts?

I want to create a function that will create a different class by passing different args ,
but I am getting an error in the code below;
It looks like even though I specify the generic, typescript can't understand it
type ClassAOptions = {
fooA: string;
barA: string;
};
class ClassA {
constructor(options: ClassAOptions) {}
static new(options: ClassAOptions): ClassA {
return new ClassA(options);
}
}
type ClassBOptions = {
fooB: boolean;
barB: boolean;
};
class ClassB {
constructor(options: ClassBOptions) {}
static new(options: ClassBOptions): ClassB {
return new ClassB(options);
}
}
const classList = {
first: ClassA,
second: ClassB,
};
function createClass<K extends keyof typeof classList>(
className: K,
args: ConstructorParameters<typeof classList[K]>[0]
) {
// type error
// Argument of type 'ClassAOptions | ClassBOptions' is not assignable to parameter of type 'ClassAOptions & ClassBOptions'.
return new classList[className](->args<-);
}
createClass("first", { fooA: "false", barA: "false" });
createClass("second", { fooB: false, barB: false });
this type error is make me crazy, does anyone know why typescript will show an error here,
I can't find my answer on google at all;
Or let me know the key point of this type err, I can google it then
The issue you are having is that Typescript will not resolve K until you actually pass in a value to createClass. In other words, it will not conditionally resolve K inside the generic function createClass, even though it is resolvable when createClass is called.
For more on this issue, see this SO answer.
You can nevertheless achieve what you want via an explicit return type (return types are resolved when the function is actually called) and an internal assertion.
function createClass<K extends keyof typeof classList>(
className: K,
args: ConstructorParameters<typeof classList[K]>[0]
): InstanceType<typeof classList[K]> {
return new classList[className](args) as InstanceType<typeof classList[K]>;
}

TS: Infer literal typing based on chained methods

There is only one thing left for me to unblock my flow and publish the first trial version of my form validation lib.
I have the following code (of course I'm omitting a lot of things so it doesn't get too big)
interface Validation {
name: string
message: string
params?: Record<string, any>
test: (value: any, params?: any) => boolean
}
class MixedSchema<T> {
type!: T
validations: Validation[] = []
required(message?: string) {
this.validations.push({
name: 'required',
message: message,
test: (value: any) => {
return value === '' ? false : true
},
})
return this
}
oneOf(arrayOfValues: any[], message?: string) {
type Params = { arrayOfValues: any[] }
this.validations.push({
name: 'oneOf',
params: { arrayOfValues },
message: message,
test: (value: any, params: Params) => {
return params.arrayOfValues.includes(value)
},
})
return this
}
}
class StringSchema extends MixedSchema<string> {
email() {
// this.validations.push({ ... })
return this
}
maxWords(maxWords: number, message?: string) {
// this.validations.push({ ... })
return this
}
}
class NumberSchema extends MixedSchema<number> {
min(min: number, message?: string) {
// this.validations.push({ ... })
return this
}
round(type: 'toUp' | 'toDown' | 'closer') {
// this.validations.push({ ... })
return this
}
}
const schema = {
string() {
return new StringSchema()
},
number() {
return new NumberSchema()
},
// array, file, etc..
}
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
type InferType<T> = {
[P in keyof T]: T[P] extends MixedSchema<infer TS> ? TS : never
}
type Form = InferType<typeof form>
So I get the following result
But I need to get the most real typing possible, I mean, similar to the schema defined for form, example
interface HowNeedsToBe {
email: string
age: number | undefined
gender: 'male' | 'female'
color: 'red' | 'blue' | 'green' | undefined
}
I believe that the logic is something like, in the absence of the required, puts an undefined and if there is oneOf, substitutes the argument with the T of the MixedSchema<T>, but I don’t know how to send this back to up to MixedSchema<T>, actually I think this logic is a mess.
I already researched about map and generics in typescript, but I confess that when it comes to putting it into practice, nothing good comes out.
Here's the TS playground if you want to try.
Conceptually you want the required() and oneOf() methods to narrow T; this would be easy enough to give types to (although you need type assertions to avoid compiler errors, since the compiler can't verify that you have actually done the requisite narrowing). So required(), called on a MixedSchema<T>, should return a MixedSchema<Exclude<T, undefined>> (using the Exclude utility type to remove undefined from any union members of T). And oneOf() should be generic in the element type U of the elements of arrayOfValues, and should return a MixedSchema<U | Extract<undefined, T>> (using the Extract utility type to keep undefined if the T can be undefined).
Here's how it might look (implementations elided for brevity):
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): MixedSchema<Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
MixedSchema<U | Extract<undefined, T>>
}
Unfortunately the fact that you are subclassing MixedSchema is complicating matters; you want to say that, for example, a StringSchema should stay a StringSchema of some kind after calling required(); it should not be widened back to MixedSchema:
declare class StringSchema<T extends string | undefined> extends MixedSchema<T> {
email(): this;
maxWords(maxWords: number, message?: string): this;
}
declare class NumberSchema<T extends number | undefined> extends MixedSchema<T> {
min(min: number, message?: string): this;
round(type: 'toUp' | 'toDown' | 'closer'): this;
}
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // MixedSchema<string>
t.email(); // error! Property 'email' does not exist on type 'MixedSchema<string>';
So we will need something more complicated.
The "right" answer here is to use so-called higher-kinded types of the sort requested (but not implemented) in microsoft/TypeScript#1213. You'd like to say something like: MixedSchema<T>'s required() method should return this<Exclude<T, undefined>>, where you are somehow treating this like a type that takes a generic parameter. So if this is a StringSchema<T>, then it should be StringSchema<Exclude<T, undefined>>. But there's no direct support for this.
Instead we need to simulate it, and all such simulations will involve some amount of "registering" the types we'd like to be able to treat like a generic-of-a-generic:
type Specify<C extends MixedSchema<any>, T> =
C extends NumberSchema<any> ? NumberSchema<Extract<T, number | undefined>> :
C extends StringSchema<any> ? StringSchema<Extract<T, string | undefined>> :
MixedSchema<T>;
We've listed out all the subclasses of MixedSchema that we care about and described how to specify their type parameters. So while we can't write this<Exclude<T, undefined>>, but we can write Specify<this, Exclude<T, undefined>> and have the same effect.
Here's the new implementation of MixedSchema:
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): Specify<this, Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
Specify<this, U | Extract<undefined, T>>
}
And we can verify that it now behaves appropriately in subclasses:
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // StringSchema<string>
t.email(); // okay
Let's make sure that the types are inferred as you'd like:
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
/* const form: {
email: StringSchema<string>;
age: NumberSchema<number | undefined>;
gender: StringSchema<"male" | "female">;
color: StringSchema<"red" | "blue" | "green" | undefined>;
} */
That's a good sign; the generic type parameter for each field has been specified the right way. And thus your InferType should be able to grab those fields types:
type Form = InferType<typeof form>
/* type Form = {
email: string;
age: number | undefined;
gender: "male" | "female";
color: "red" | "blue" | "green" | undefined;
} */
Looks good!
Playground link to code

Encapsulate WebSocket Message in Typescript

I am trying to encapsulate websocket messages into well defined Type.
I have main IIncommingMessage which is the base interface for all incoming messages as such:
export interface IIncommingMessage {
className : IClassName;
methodName : IMethodName;
}
There are various types of class this websocket can call as follows:
export type IClassName = IClassA | IClassB | IClassC
as well as various method in associated classes
export type IMethodName = IfooinClassA | IbarinClassA | IbazinClassB | IquxinClassB | IquuxinClassB | IcorgeinClassC
Such that it looks like this
ClassA:
foo()
bar()
ClassB:
baz()
qux()
quux()
ClassC:
corge
The idea is that if a websocket message arrives. It'll come as
{
className : "ClassB"
methodName : "qux"
}
So this should call ClassB of function qux().
The approach I'm taking looks bad. Was wondering if there is a better way to tightly couple the web-socket message to a well defined type
Also curious on how i'll make this call in TypeScript - would it be protoype.call('className.method')?
About your first part to have well defined type this is how I would implement it.
class ClassA {
foo() { }
bar() { }
}
class ClassB {
baz() { }
qux() { }
quux() { }
}
class ClassC {
corge() { }
notAMethod = 1;
}
// This will be a like a dictionary mapping name to class
// Will also be used in the second part.
const Classes = {
ClassA,
ClassB,
ClassC,
};
// This will be 'ClassA'|'ClassB'|'ClassC'
type IClassName = keyof typeof Classes;
type IClassOf<T extends IClassName> = InstanceType<typeof Classes[T]>;
type MethodFilter<T extends IClassName> = { [MN in keyof IClassOf<T>]: IClassOf<T>[MN] extends () => void ? MN : never }
type MethodName<T extends IClassName> = MethodFilter<T>[keyof MethodFilter<T>];
interface IGenericIncomingMessage<T extends IClassName> {
className: T;
methodName: MethodName<T>;
}
type IIncomingMessage = IGenericIncomingMessage<'ClassA'> | IGenericIncomingMessage<'ClassB'> | IGenericIncomingMessage<'ClassC'>;
let msg0: IIncomingMessage = {
className: 'ClassA',
methodName: 'foo', // valid
}
let msg1: IIncomingMessage = {
className: 'ClassC',
methodName: 'corge', // valid
}
let msg2: IIncomingMessage = { // compiler error. Type ... is not assignable to type 'IIncomingMessage'.
className: 'ClassA',
methodName: 'corge',
}
let msg3: IIncomingMessage = {
className: 'ClassD', // compiler error. ClassD Name is not not in 'ClassA' | 'ClassB' | 'ClassC'
methodName: 'corge',
}
let msg4: IIncomingMessage = {
className: 'ClassC',
methodName: 'notAMethod', // compiler error. Type '"notAMethod"' is not assignable to type '"foo" | "bar" | "baz" | "qux" | "quux" | "corge"'.
}
So about the second part I use the Classes dictionary I defined earlier to lookup class by name, create a new instance of the class. This means that a malicious message having a valid class name that is not in the dictionary will not work.
// I omit error handling here.
function invokeFunction<T extends IClassName>(message: IGenericIncomingMessage<T>): void {
// Look for the class instance in dictionary and create an instance
const instance = new Classes[message.className]() as IClassOf<T>;
// Find the method by name. The cast to any is to silence a compiler error;
// You may need to perform additional login to validate that the method is allowed.
const fn = instance[message.methodName] as unknown as () => void;
fn.apply(instance);
}

Difference between the static and instance sides of classes

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

Categories

Resources