How to do a runtime cast - javascript

I'm on a web application that I write in TypeScript. In one part of the application, the use can write an additional JavaScript function, that will be parsed at runtime (new function(Function as String)) for execution. The code will give me back an object which I defined in TypeScript as class Script. This script object should contain specific functions, otherwise it's invalid. Meaning, I want to throw an exception, if one or more functions are not implemented by the user.
A Typescript cast won't do it, as it is a compile time cast.
I thought about giving the Script object a constructor that takes the parsed object (that, by key/values, should be a Scriptobject already) and check the object in the constructor for missing properties.
Something like this:
(This will not work, it only should show the idea)
export class Script {
constructor(object: Script) {
this.getDefaultValue = object.getDefaultValue;
this.isAvailable = object.isAvailable;
this.isValid = object.isValid;
this.isInRange = object.isInRange;
this.isDataFormat = object.isDataFormat;
for (let propertie in this){
if (!this[propertie]){
throw new Error(propertie+ ' is missing.');
}
}
}
getDefaultValue: any;
isAvailable: (containerSetId: number) => boolean;
isValid: (value: any) => boolean;
isInRange: (value: any) => any;
isDataFormat: (value: any) => boolean;
}
But isn't there a nicer way to do this?

You can't use that because:
class A {
member1: string;
member2: number;
member3: boolean;
constructor() {
this.member3 = true;
}
}
Compiles into:
var A = (function () {
function A() {
this.member3 = true;
}
return A;
}());
As you can see, the member1 and member2 are not part of the compiled js version.
You'll need to keep track of the required properties for runtime, something like:
class Script {
getDefaultValue: any;
isAvailable: (containerSetId: number) => boolean;
isValid: (value: any) => boolean;
isInRange: (value: any) => any;
isDataFormat: (value: any) => boolean;
required = [
"getDefaultValue",
"isAvailable",
"isValid",
"isInRange",
"isDataFormat"
]
constructor(object: Script) {
this.getDefaultValue = object.getDefaultValue;
this.isAvailable = object.isAvailable;
this.isValid = object.isValid;
this.isInRange = object.isInRange;
this.isDataFormat = object.isDataFormat;
for (let propertie in this.required) {
if (!this[propertie] || typeof this[propertie] !== "function") {
throw new Error(propertie+ ' is missing.');
}
}
}
}

Related

TypeScript Mixins and Constructor Names

I have this sample of code experimenting with mixins in TypeScript. However, it is not returning what I am expecting.
It should give me: User ({"id":3,"name":"Lorenzo Delaurentis"}).
Instead, I am getting: Function ({"id":3,"name":"Lorenzo Delaurentis"}).
The line let Name = Class.constructor.name should give me User, but it is not. Am I missing something obvious here?
type ClassConstructor<T> = new(...args: any[]) => T
function withDebug<C extends ClassConstructor<{
getDebugValue(): object
}>>(Class: C) {
return class extends Class {
constructor(...args: any[]) {
super(...args)
}
debug() {
let Name = Class.constructor.name
let value = this.getDebugValue()
return `${Name} (${JSON.stringify(value)})`
}
}
}
class DebugUser {
constructor(
private id: number,
private firstName: string,
private lastName: string
) {}
getDebugValue() {
return {
id: this.id,
name: `${this.firstName} ${this.lastName}`
}
}
}
let User = withDebug(DebugUser)
let user = new User(3, 'Lorenzo', "Delaurentis")
console.log(user.debug())
P.S. I compiled with tsc mixins --target ES6. Otherwise, I get an error: error TS2339: Property 'name' does not exist on type 'Function'.
You want just Class.name. The Class.constructor is Function.

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 use an interface for function type declaration in type script

I am trying create a function type (delegate), by using following code.
interface DelegateTypeExample {
m: (s: string) => number;
}
const someFunc: DelegateTypeExample = (s: string) => {
return { m: null };
};
I keep getting following error, even though I am returning m in my object.
Property 'm' is missing in type '(s: string) => { m: any; }' but required in type 'DelegateTypeExample'.
Now I am able to get rid of the error by using :
interface DelegateTypeExample {
(s: string): number;
}
const someFunc: DelegateTypeExample = (s: string) => {
return 2;
};
But the moment I am adding another field to interface, it stops working, even thoug I am returning anotherField in the object.
interface DelegateTypeExample {
(s: string): number;
anotherField: string;
}
const someFunc: DelegateTypeExample = (s: string) => {
return { x: 2, anotherField: '123' };
};
error:
Property 'anotherField' is missing in type '(s: string) => { x: number; anotherField: string; }' but required in type 'DelegateTypeExample'.
Just wanted to understand, why my code won't work , the way it is now.
Is it just a syntactic principle , I have to follow.
You are misunderstanding the role of the interface. When some object implements an interface, it must have all the fields and functions which the interface has. In an essence, the interfaces says "all object implementing me will have at least all of the stuff I'm declaring" - it's a contract between the interface and its users.
Now back to your examples:
interface DelegateTypeExample {
// Your interface must have a function called m which takes a string and returns a number
m: (s: string) => number;
}
// You are saying that your someFunc is a function which takes a string
// and returns an object which has the field m.
const someFunc: DelegateTypeExample = (s: string) => {
return { m: null };
};
As you can see from my comments, you are creating 2 very different types and that's why they are incompatible. Here's what the current definition of the interface is expecting:
const someObj: DelegateTypeExample = {
m: (s: string) => 42
};
Moving on to your second example, it works because now you are saying that your interface is entirely a function - it doesn't have a function called m - the entire object must be a function. That's why it works.
Finally, your third example says that objects implementing the interface must:
Be a function
Have a anotherField property. Note that it's the function which has to have the property, and not its return value.
Here's how a valid object would look like for your third example.
// You can't add all fields of the type immediately since it needs to be a function
const someFunc: DelegateTypeExample = (s: string) => {
return 42;
};
// Add the field
someFunc.anotherField = "Foo";
As a general tip, when creating objects that follow a certain interface, make sure that the object has exactly properties as the interface.
If you add field to your DelegateTypeExample you define a property to be assigned to the someFunc variable and not a return type of the sumeFunc function.
The following example would not throw an error:
interface DelegateTypeExample {
(s: string): number;
anotherField: string;
}
const someFunc: DelegateTypeExample = (s: string) => {
return 2;
};
someFunc.anotherField = '123'˙;
Basically this is a way to add static fields to the function constructor, and it would be accessible through someFunc.anotherField.
If you want to add this field to the return value, you have to extend your return type:
interface DelegateTypeExample {
(s: string): {x: number, anotherField: string;};
}
const someFunc: DelegateTypeExample = (s: string) => {
return { x: 2, anotherField: '123' };
};

Javascript nameof object property

In C# there is the posibility to get the name of an object property as a string value.
nameof(object.myProperty) --> "myProprty"
Can this be done in Javascript/Typescript?
Object.Keys() is the only thing i found, but it gives me all the keys.
Example what I want to achieve:
export interface myObject {
myProperty: string;
}
console.log(myObject["myProperty"]);
Let's say I change my interface for some reason to:
export interface myObject {
myPropertyOther: string;
}
console.log(myObject["myProperty"]); // This will not give an error on build
So I want to have something like this:
console.log(myObject[nameOf(myObject.myProperty)]
// This will give an error on build when the interface is changed
There is no nameof operator in Javascript/Typescript. You can creat a function that takes the key of another object and this is checked by the typescript compiler:
function keyOf<T>(o: T, k: keyof T) {
return k
}
let o = { a: 1 }
keyOf(o, 'a'); //ok
keyOf(o, 'b'); //err
I made a library that fetches the name of a property at runtime, even for types that don't exist at runtime (interfaces or types in TypeScript):
It can be found on NPM here: #fluffy-spoon/name-of
The source code is simple (just a few lines of code): https://github.com/ffMathy/FluffySpoon.JavaScript.NameOf
Usage
import { getPropertyName, getDeepPropertyName } from '#fluffy-spoon/name-of';
interface SomeType {
foo: boolean;
someNestedObject: {
bar: string;
}
}
console.log(getPropertyName<SomeType>(x => x.foo)); //prints "foo"
console.log(getPropertyName<SomeType>(x => x.someNestedObject)); //prints "someNestedObject"
console.log(getPropertyName<SomeType>(x => x.someNestedObject.bar)); //prints "bar"
console.log(getDeepPropertyName<SomeType>(x => x.foo)); //prints "foo"
console.log(getDeepPropertyName<SomeType>(x => x.someNestedObject)); //prints "someNestedObject"
console.log(getDeepPropertyName<SomeType>(x => x.someNestedObject.bar)); //prints "someNestedObject.bar"
Library source code
In case you don't want to install the NPM package.
function getPropertyNameInternal<T = unknown>(expression: (instance: T) => any, options: {
isDeep: boolean
}) {
let propertyThatWasAccessed = "";
var proxy: any = new Proxy({} as any, {
get: function(_: any, prop: any) {
if(options.isDeep) {
if(propertyThatWasAccessed)
propertyThatWasAccessed += ".";
propertyThatWasAccessed += prop;
} else {
propertyThatWasAccessed = prop;
}
return proxy;
}
});
expression(proxy);
return propertyThatWasAccessed;
}
export function getPropertyName<T = unknown>(expression: (instance: T) => any) {
return getPropertyNameInternal(expression, {
isDeep: false
});
}
export function getDeepPropertyName<T = unknown>(expression: (instance: T) => any) {
return getPropertyNameInternal(expression, {
isDeep: true
});
}
Yes, this can be done in Javascript/Typescript. There is a typescript module.
Shamelessly copied from the manual.
nameof(console);
nameof(console.log);
nameof(console["warn"]);
Transforms to:
"console";
"log";
"warn";
There are more nice examples in the manual.
The solution for yours question:
interface IMyObject {
myPropertyOther: string;
}
let myObject: IMyObject = {
myProperty: 'Hello world'
};
console.log(myObject[nameof<IMyObject>((o) => o.myProperty)]);

Properly type dynamic functions in a proxy using TypeScript

I have a function that returns a Proxy which uses a get trap to infer functions from multiple objects. Now I need to be able to type these functions, since they are technically known at compile time but TypeScript just can't figure them out. How do make TypeScript recognize these functions to be able to add them to my interface?
interface IObject {
(): IFunction;
addAMethod(name: string, fn: (...args: any[]) => any): void;
}
interface IFunction {
list: string[];
option: boolean;
}
const mainFunc: IObject = Object.assign(
(): IFunction => {
const context = {
list: [],
option: true,
};
return new Proxy(context, handler);
},
{
addAMethod(name: string, fn: (...args: any[]) => any) {
Object.assign(core, fn);
},
},
);
const handler = {
get(context: {[key: string]: any}, prop: string, receiver: IFunction) {
if (prop in context) {
return context[prop];
}
if (prop in core) {
return core[prop];
}
},
};
const core = {
evaluate: (val: any) => true,
doSomething: (val: any) => false
}
export default mainFunc;
Now this would allow one to the following:
import mainFunc from "./mainFunc"
mainFunc().evaluate('wow'); // Should return true
This of course does not return true but rather shows that evaluate() is not part of type IFunction, which is true. But when I try to add it I get another error, where I can't assign the function to IFunction because it does not define evaluate().
// ...
interface IFunction {
list: string[];
option: boolean;
evaluate(val: any): boolean;
}
const mainFunc: IObject = Object.assign(
(): IFunction => {
const context = {
list: [],
option: true,
};
return new Proxy(context, handler); // Error happens here
},
// ...
);
// ...
const core = {
evaluate: (val: any) => true,
doSomething: (val: any) => false
}
Type '{ list: never[]; option: boolean; }' is not assignable to type 'IFunction'.
Property 'evaluate' is missing in type '{ list: never[]; option: boolean; }'.
Any idea how I could type evaluate()?
The problem is that the TypeScript typings for the Proxy constructor assumes that the proxy will be the same type as the target parameter, even though this is not always true. There is an open issue asking for community help in coming up with better typings.
In the absence of that, I think the only way to deal with this is going to be the judicious use of type assertions and the any type. For example, you can change the line with the error to:
return new Proxy(context, handler) as IFunction;
and that will probably silence the error. It's not fully type safe, but as long as you're sure about what you're doing, it should be fine. And you should be able to add that evaluate() method to the IFunction interface too.
Hope that helps; good luck!

Categories

Resources