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

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

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.`);
}
}
};

Typescript class getters that validate property presence

I'm enjoying a new (to me) pattern of creating data models as classes from api responses as they can have reusable derived logic that shouldn't necessarily be returned from the api.
My issue is having multiple getters that I would like to assert existence of a property when referencing each other (or in fact used in other files) but don't, so I'm wondering what I'm missing or what the better solution is:
type TodoResponse = {
userId: number;
id: number;
title: string;
completed: boolean;
authors?: string[];
}
class Todo implements TodoResponse {
userId!: number;
id!: number;
title!: string;
completed!: boolean;
authors?: string[];
constructor(todo: TodoResponse) {
Object.assign(this, todo);
}
get hasAuthors(): boolean {
return Boolean(this.authors?.length);
}
get firstAuthor(): string | void {
if (this.hasAuthors) return this.authors[0]
// errors with: Object is possibly 'undefined'
}
get firstAuthor(): string | void {
if (this.authors?.length) return this.authors[0]
// this works but it feels redundant to duplicate logic from other getters
}
}
What you want is for a check of todo.hasAuthors to act as a type guard which can be used to narrow the type of todo to something that is known to contain a defined authors property. Unfortunately TypeScript does not currently have a way to achieve this.
First, classes cannot be seen to implement discriminated union types; otherwise you could have Todo be assignable to {hasAuthors: true, authors: string[]} | {hasAuthors: false, authors?: undefined}. You could maybe use type assertions to have Todo look like this from the outside:
interface TodoWithAuthors extends TodoResponse {
hasAuthors: true,
authors: string[],
firstAuthor: string
}
interface TodoWithoutAuthors extends TodoResponse {
hasAuthors: false,
authors?: undefined,
firstAuthor: void
}
type Todo = TodoWithAuthors | TodoWithoutAuthors;
const Todo = class Todo implements TodoResponse {
/* snip, your original impl goes here */
} as new (todo: TodoResponse) => Todo;
const todo = new Todo({
id: 1, userId: 2, title: "",
completed: false, authors: Math.random() > 0.99 ? undefined : ["a", "b"]
});
if (todo.hasAuthors) {
// no compiler errors here
console.log(todo.authors.join(", ")) // a, b
console.log(todo.firstAuthor.toUpperCase()) // A
}
But from the inside, the compiler could not see this.hasAuthors as having any effect on this.authors. So this doesn't help you the way you want.
TypeScript does have the concept of user-defined type guard functions and methods, where you can call a boolean-returning function or method, and it will act as a type guard on one of its input arguments (or on the this context of a method). So if hasAuthors were a method instead of a getter, you could do something like this:
class Todo implements TodoResponse {
userId!: number;
id!: number;
title!: string;
completed!: boolean;
authors?: string[];
constructor(todo: TodoResponse) {
Object.assign(this, todo);
}
hasAuthors(): this is { authors: string[], firstAuthor: string } {
return Boolean(this.authors?.length);
}
get firstAuthor(): string | void {
if (this.hasAuthors()) return this.authors[0] // okay
}
}
By annotating the return type of hasAuthors() as this is { authors: string[], firstAuthor: string }, I'm saying that a true result of obj.hasAuthors() will narrow the type of obj to something with a defined string[] property (and also a defined firstAuthor property). This works inside the implementation of firstAuthor(). It also works outside the class:
const todo = new Todo({
id: 1, userId: 2, title: "",
completed: false, authors: Math.random() > 0.99 ? undefined : ["a", "b"]
});
if (todo.hasAuthors()) {
// no compiler errors here
console.log(todo.authors.join(", ")) // a, b
console.log(todo.firstAuthor.toUpperCase()) // A
}
So, that's great. Unfortunately there is no analogous user-defined type guard property or accessor method functionality. There's an open feature request at microsoft/TypeScript#43368, whose status is currently "Awaiting More Feedback". That means they probably won't even consider implementing such a feature unless they hear about some compelling use cases from the community. If you want to see this happen you might consider going to that issue, giving it a 👍, and explaining why this would be better than the current options. But even if you do that there's no telling when or even if such a feature would be available.
So for now you're kind of stuck. Either you use redundant type guards of the form if (this.authors?.length) return this.authors[0], or you use type assertions like if (this.hasAuthors) return this.authors![0], or you refactor away from your desired implementation.
Playground link to code

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

Detecting method signature in Typescript?

Suppose we have something like this:
class TodoStore {
query(state:State):Array<Observable<any>> {
return state.getAll(); //Custom implementation
}
}
At runtime I'd like to find all the methods that have a State argument as the first argument, so that these can be wrapped by a function that passes in the state argument. Is this possible with Typescript?
The wrapper class would work approximately like this:
class TodoStoreWrapper {
constructor(private state:State);
todoStore: TodoStore = new TodoStore();
query() {
todoStore.query(state);
}
}
While at runtime we don't have the type information to find out which functions have a state parameter, we can at compile time create a type that by its structure requires us to specify in an object literal all methods that have a first argument of type state. While this approach does require us to specify the method it is safe, as the compiler will throw an error if we forget a method and will also throw an error if we specify a name that is not such a method.
We can also combine this with a type that removes the state parameter from all methods that have it :
class TodoStore {
query(state: State): Array<Observable<any>> {
return state.getAll(); //Custom implementation
}
queryWithParam(state: State, p:string): Array<Observable<any>> {
return state.getAll(); //Custom implementation
}
}
type StateMethods<T> = Record<{
[P in keyof T]: T[P] extends ((s:State, ...a:any[])=> any) ? P : never
}[keyof T], true>
type Wrapped<T> = {
[P in keyof T]:
T[P] extends ((s: State, ...a: infer A) => infer R) ?
(...a: A) => R :
T[P]
}
function createWrapper<T>(toWrap:T, methods: StateMethods<T>): Wrapped<T>{
return null as any;
}
let w = createWrapper(new TodoStore(), { // This object will be checked by the compiler to contain the keys of all relevant methods
query: true,
queryWithParam: true
});
w.query()
w.queryWithParam("")
A decorator approach is also possible and you should see which fits better for your use case, but this approach can work as well.
Playground link

TypeScript: define dynamic properties in proxy

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!

Categories

Resources