TypeScript: define dynamic properties in proxy - javascript

I have the following call to a watch
const watch = hp.watch({
running: false,
time: 0,
start: Date.now()
})
watch bassically just runs new proxy(), then sets some properties and returns the newly created proxy class nothing too fancy.
export function watch(item: { [key: string]: any }): proxy
export function watch(key: string, value: any): proxy
export function watch(...args: any[]): proxy {
let prox = new proxy()
if (args.length == 2) {
prox[args[0]] = args[1]
} else if (args.length == 1 && args[0] instanceof Object) {
for (let itm in args[0]) {
!(itm in prox) && (prox[itm] = args[0][itm])
}
}
return prox
}
I then have an interface which looks like this:
export interface proxy {
[key: string]: any
}
Here is the the proxy class which basically is just a wrapper.
namespace hp {
export class proxy {
public constructor() {
return new Proxy(this, { /* Proxy stuff */})
}
}
}
In an editor that supports intellisense, it would be nice if I could have it suggest running, time, start after I type watch..
I think I need to use a more advanced interface (or type) than the one I am using for that to happen. I have tried this but it doesn't work:
export type watch<T> = {
[A in keyof T]: T[A]
}
export interface proxy {
[key: string]: watch<any>
}
When doing watch.time = 123 I get an error stating:
Type 'number' is not assignable to type 'watch'.
and when trying to get the value let a = watch.time I get this error:
The right-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.

You want to change the signature for hp.watch() to something like
export function watch<T>(item: T): proxy & T;
export function watch<K extends string, V>(key: K, value: V): proxy & Record<K, V>;
export function watch(...args: any[]): proxy {
// impl
}
Then you have told TypeScript that the output of the function is both a proxy and has the same keys and value types as the thing you passed in.
Hope that helps; good luck!

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]>;
}

how forcing a type instance instead of typeof in typescript

how can I export a type by forcing it to be an instance.
I have tried many ways, I only found one solution, creating a static getter but I would like to remove my static getter.
Here context:
I would like to export a type of a instance of A from there $A.A, for ref only.
export const $A = (() => {
class A {
static get default() {
return A.create();
}
static create() {
return new A();
}
constructor() {}
}
return { A };
})();
i try many way, here 7 of them ! no one work instead the way 1 ! but it because i add a static getter in the js class.
export type _1 = typeof $A.A.default;
export type _2 = typeof new $A.A;
export type _3 = typeof $A.A.create();
export type _4 = typeof $A.A();
export type _5 = typeof $A['A'];
export type _6 = $A.A;
export type _7 = typeof new ()=>$A.A;
// example somewhere in the project, i want tell A should be a instance and not a typeof!
function foo(A:_6)
So what the syntax to emulate a instance in a ts type for export somewhere for typage usage only.
My project is in js, but using ts only for help the tsserver to understand my refs when he dont.
So it for Intelisence in my ide only and no for generate ts=>js.
Preliminary note: the class A code here lacks any instance structure (no properties or methods). All non-nullish values will be assignable to that instance type; see this FAQ entry for more info. Just to avoid this weirdness, I've added a property to the example class:
const $A = (() => {
class A {
static get default() {
return A.create();
}
static create() {
return new A();
}
constructor() { }
someStructure = 123; // add structure here
}
return { A };
})();
Now the compiler can tell that {someRandomThing: 123} is not compatible with the A type you're having trouble naming.
You might want to use the InstanceType<T> utility type to pull out the return type of a construct signature:
type A = InstanceType<typeof $A.A>
You could write this yourself using conditional type inference:
type AlsoA = typeof $A.A extends new (...args: any) => infer I ? I : never;
Or, you could use the method we had to use before conditional types existed: TypeScript pretends that the prototype property of a class is the same as its instance type. This isn't really true since the prototype generally only contains the methods and not other properties. But you can use it anyway:
type AlsoAlsoA = typeof $A.A.prototype;
Any of those should produce the same type.
Let's make sure it works:
function foo(a: A) { }
foo($A.A.create()) // okay
foo({ someRandomThing: 123 }) // error
// Argument of type '{ someRandomThing: number; }' is
// not assignable to parameter of type 'A'.
Looks good!
Playground link to code

In typescript generics what is use of '=' operator [duplicate]

I have the following logging method:
private logData<T, S>(operation: string, responseData: T, requestData?: S) {
this.logger.log(operation + ' ' + this.url);
if (requestData) {
this.logger.log('SENT');
this.logger.log(requestData);
}
this.logger.log('RECEIVED');
this.logger.log(responseData);
return responseData;
}
The requestData is optional. I want to be able to call logData without having to specify the S type when I don't send the requestData to the method: instead of: this.logData<T, any>('GET', data), I want to call this.logData<T>('GET', data).
Is there a way to achieve this?
As of TypeScript 2.3, you can use generic parameter defaults.
private logData<T, S = {}>(operation: string, responseData: T, requestData?: S) {
// your implementation here
}
TS Update 2020:
Giving void will make the generic type optional.
type SomeType<T = void> = OtherType<T>;
The answer above where a default value as the object is given make it optional but still give value to it.
Example with default type value is {}:
type BaseFunctionType<T1, T2> = (a:T1, b:T2) => void;
type FunctionType<T = {}> = BaseFunctionType<{name: string}, T>
const someFunction:FunctionType = (a) => {
}
someFunction({ name: "Siraj" });
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Expected 2 arguments, but got 1.(2554)
Playground Link
Example with default generic type value is void
type BaseFunctionType<T1, T2> = (a:T1, b:T2) => void;
type FunctionType<T = void> = BaseFunctionType<{name: string}, T>
const someFunction:FunctionType = (a) => {
}
someFunction({ name: "Siraj" })
This is a good read on making generics optional.
Optional generic type in Typescript
Playground Link
As per TypeScript 2.2 (you can try it in the TS Playground), calling this.logData("GET", data) (with data of type T) gets inferred succesfully as this.logData<T, {}>("GET", data).
The overload suggested by David Bohunek can be applied if the inference fails with the TS version you use. Anyway, ensure that the second signature is before declared and then defined, otherwise it would not participate in the available overloads.
// Declarations
private logData<T>(operation: string, responseData: T);
private logData<T, S>(operation: string, responseData: T, requestData?: S);
// Definition
private logData<T, S>(operation: string, responseData: T, requestData?: S) {
// Body
}
If you're looking for an optional generic type within a Type/Interface declaration, this might help.
(came looking for this, only found answers dealing with generic function declarations. Siraj's answer got me on the right track.)
type ResponseWithMessage = {
message: string;
};
interface ResponseWithData<T> extends ResponseWithMessage {
data: T;
}
export type ResponseObject<T = void> = T extends void
? ResponseWithMessage
: ResponseWithData<T>;
How about Partial?
Constructs a type with all properties of Type set to optional. This utility will return a type that represents all subsets of a given type.
You can write the overloading method like this:
private logData<T>(operation: string, responseData: T);
private logData<T, S>(operation: string, responseData: T, requestData?: S) {
this.logger.log(operation + ' ' + this.url);
if (requestData) {
this.logger.log('SENT');
this.logger.log(requestData);
}
this.logger.log('RECEIVED');
this.logger.log(responseData);
return responseData;
}
But I don't think you really need it, because you don't have to write this.logData<T, any>('GET', data) instead just write this.logData('GET', data). The T type will be infered
void doesn't play well where some kind of object is expected. Edit: plays perfectly well, you just have to consider the fact that
void will override anything you merge it with (void & string is
basically void) - this is probably the one you want
unknown
{} basically the same as unknown
interface OffsetPagination {
offset: number;
limit: number;
}
interface PagePagination {
page: number;
size: number;
}
type HttpListParams<
PaginationType extends OffsetPagination | PagePagination | void = void,
Params = {
filter?: string;
},
> = PaginationType extends void ? Params : PaginationType & Params;
ts playground

Declare interface of class only with methods signature without names

Suppose I have a class with many methods, but I know for sure that their signature matches.
Is it possible to describe the interface of this class without describing the specific methods of this class in it? Like here:
interface IController {
(input: string): number // any method without reference to its name
}
class Controller implements IController {
method1(input: string): number { ...do something }
method2(input: string): number { ...do something }
...
}
Or is it impossible?
The option to have an index signature (as #fk82 outlines in his answer) has the undesired consequence of forcing you to add an index signature to the class. This means that your class will be indexable by an arbitrary string, which might not be what you want.
If your goal is just to force the implementer class to only have methods with the given signature, a better option is to use a mapped type:
type IController<K extends PropertyKey> = {
[P in K]: (input: string) => number;
}
class Controller implements IController<keyof Controller> {
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
}
let a = new Controller();
a['aa'] // not allowwed under no implicit any
This has the bonus advantage of allowing the class to have some methods that do not conform to the signature if needed, but in an explicit way:
class Controller implements IController<Exclude<keyof Controller, 'special'>> {
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
special() { }
}
You can use an index signature
interface IController {
[name: string]: (input: string) => number;
}
A small caveat is that the TypeScript compiler will now require you to add the index signature to each class which implements IController. I.e. you need to define your Controller class as follows:
class Controller implements IController {
[name: string]: (input: string) => number;
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
}
Here's a TS playground with a full example. Note that the index signature will be tested in assertions such as
const A = {
m(input: string): number { return input.length; },
} as IController;
const B = {
m(input: string): string { return input; }
} as IController;
and the assignment of B will will raise a type error because of the string return value.
You can hack something in order to fit your sought out solution, as per #FK82's answer, but that'd defeat the purpose of the Interface construct, which is to bind an object to a bunch of compile-time known signatures. How would the compiler know what method in particular you are referring to when referencing the Interface?
However, from what I can tell, instead of trying to cram an Interface-based solution, why not declare a functional abstraction in your executing code instead? Just describe the function signature and swap to the proper method as you see suit, since in JS/TS functions are a First-class citizen.
type Strategy = (input: string) => number;
class Controller implements IController {
method1(input: string): number { ...do something }
method2(input: string): number { ...do something }
}
function useMethod(f: Strategy): any {
...
const i = f('my string');
...
}
function main() {
const c = new Controller ();
const method = chooseOne === true ? c.method1 : c.method2;
useMethod (method);
}
This way of doing things is not too disimilar to the Strategy Pattern in OOP, however the FP solution is leaner and boasts what's in my opinion one of the better features of Javascript/Typescript.

Categories

Resources