Detecting method signature in Typescript? - javascript

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

Related

Next arguments of function definition depending on first argument

Consider a list of simple functions with different arguments:
const fns = {
isValidDate: (input: string, min?: Date, max?: Date): boolean => {
// ...
return true;
},
isValidOption: (input: string, options: string[]): boolean => {
// ...
return true;
},
};
They all return the same type (bool);
Then another function that is supposed to call any of the functions above:
function validateField(where: string, fn: keyof typeof fns, ...args: any[]){
// ...
return fns[fn](...args);
}
How can I make args reflect the parameters of the chose fn ?
For example:
validateField("test", "isValidDate", new Date()); // should be ok
validateField("test", "isValidDate", 123); // should fail
and have the arguments show in vscode hints, like on normal functions.
I know I need to create overloads for validateField for each fn, but how to do that with a type definitions or something... without having to manually define each overload and write duplicate code with those arguments
You probably want validateField() to be generic in the type of the fn parameter so you can choose the appropriate type for args. We can write some helper utility types to compute this:
type Fns = typeof fns;
type FnArgs = { [K in keyof Fns]:
Fns[K] extends (input: string, ...args: infer A) => boolean ? A : never
};
/* type FnArgs = {
isValidDate: [min?: Date | undefined, max?: Date | undefined];
isValidOption: [options: string[]];
} */
The FnArgs type is a mapped type where each key comes from the type of fns, and each value is the tuple of parameters after the initial string (using conditional type inference to get that list).
Now you can give validateField() this call signature:
declare function validateField<K extends keyof Fns>(
where: string, fn: K, ...args: FnArgs[K]
): boolean;
and it will work when you call it:
validateField("test", "isValidDate", new Date()); // okay
validateField("test", "isValidDate", 123); // error! number is not assignable to date
validateField("test", "isValidOption", ["a"]) // okay
Unfortunately the implementation of validateField() does not type check:
function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
return fns[fn](where, ...args); // error!
// -----------------> ~~~~~~~
// A spread argument must either have a tuple type or
// be passed to a rest parameter.
}
The underlying issue is lack of direct support for correlated unions as requested in microsoft/TypeScript#30581. The compiler is not able to understand that the type of fns[fn] is of a function type that is correlated with the type of args. The error message is a bit cryptic, but it comes from the fact that it sees args as a union of tuple types inappropriate for the arguments of fns[fn], which it sees as a union of function types without a common rest parameter type.
Luckily there's a recommended solution for this described in microsoft/TypeScript#47109. We need to give fns a new type that the compiler can see at a glance is an object with methods whose parameters are directly related to FnArgs. Here's how it looks:
function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
const _fns: { [K in keyof Fns]: (str: string, ...args: FnArgs[K]) => boolean } = fns;
return _fns[fn](where, ...args); // okay
}
The _fns variable is annotated as being of a mapped type of methods explicitly with a rest parameter of tpe FnArgs[K] for every K in the keys of Fns. The assignment of fns to that variable succeeds, because it's the same type.
But the crucial difference is that _fns[fn](where, ...args) succeeds where fns[fn](where, ...args) fails. And that's because the compiler has kept track of the correlation across the generic K between the type of _fns[fn] and the type of args.
And now you have something that works as desired for both the callers and the implementation of the function!
Playground link to code
To solve your question, you can use the Generic type to get the function type and then use this to get the type of the parameter
function validateField<key extends keyof typeof fns>(where: string, fn: key, ...options: Parameters<typeof fns[key]>): boolean {
const fnToCall = fns[fn];
return fnToCall(...options);
}

Typescript: Mapping Tuples to Union types doesn't seem work inside nested objects

I have a Vue project in Typescript, and I'm running into an issue regarding the mapping of an Object of Tuples to an Object of Union types.
For a bit of context, I'm working on a Backend endpoint's expected response types. Currently, this endpoint receives 2 values: an enum, and a string. Depending on the enum the response object will change.
This is the current implementation:
const validations = {
email: ['isValid', 'isAvaliable'],
password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
iban: ['isValid', 'isRegistered']
} as const
type ValidationsMap = {
[T in keyof typeof validations]: typeof validations[T][number]
}
function validate<T extends keyof ValidationsMap>(type: T, value: string): Record<ValidationsMap[T], boolean> {
// Do something
}
Now the Backend endpoint will receive one more parameter, and the response object will depend on it too.
This is what I've tried, but it's not working:
const validations = {
portal: {
email: ['isValid', 'isAvaliable'],
password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
},
payment: {
email: ['isValid'],
iban: ['isValid', 'isRegistered']
}
} as const
type ValidationsMap = {
[S in keyof typeof validations]: {
[T in keyof typeof validations[S]]: typeof validations[S][T][number] // Error: Type 'number' cannot be used to index type...
}
}
function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(service: S, type: T, value: string): Record<ValidationsMap[S][T], boolean> {
// Do something
}
Does anyone know why this doesn’t work?
I thought there might be a limit to how deep it will allow to map a Tuple to a Union, but it doesn't seem to be the case.
This works correctly:
type PortalEmailValidation = typeof validations['portal']['email'][number]
This seems to be a bug in TypeScript, as described in microsoft/TypeScript#27709. The type checker apparently doesn't properly track the constraints for deep index access types when the keys are generic. That issue has been open for a long time with no sign of progress, so for now all we can do is work around it.
One approach when the compiler won't accept an index access of the form T[K] is to use conditional type inference, like T extends Record<K, infer V> ? V : never (using the Record<K, V> utility type). If K is a (non-optional) key of T, then T will be seen as a Record<K, V> for some V, which we infer.
So instead of typeof validations[S][T][number], we can write typeof validations[S][T] extends Record<number, infer V> ? V : never. Or equivalently:
type ValidationsMap = {
[S in keyof typeof validations]: {
[T in keyof typeof validations[S]]:
typeof validations[S][T] extends { [k: number]: infer V } ? V : never
}
}
Another approach is to explicitly add back in the missing constraint. If you have a type A that you know is assignable to another type B but the compiler doesn't know this, then you can replace A with Extract<A, B> (using the Extract<T, U> utility type) and the compiler will accept it. It knows Extract<A, B> is assignable to B. And assuming you're right about A being assignable to B, then Extract<A, B> will evaluate to just A.
So if ValidationsMap[S][T] is assignable to string but the compiler can't see it, we can write Extract<ValidationsMap[S][T], string> instead:
function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(
service: S, type: T, value: string
): Record<Extract<ValidationsMap[S][T], string>, boolean> { // okay
return null!
}
Playground link to code

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

Property is missing in type but required in type

I'm trying to implement simple event dispatcher on TypeScript.
My custom types are declared like this:
events.d.ts:
type AppEvent = { [key: string]: any }
type AppEventListener<T> = <T extends AppEvent>(event: T) => void;
When I pass any object in this function
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>)
I get the following error:
Argument of type '(event: TestEvent) => void' is not assignable to parameter of type 'AppEventListener<typeof TestEvent>'.
Types of parameters 'event' and 'event' are incompatible.
Type 'T' is not assignable to type 'TestEvent'.
Property 'name' is missing in type 'AppEvent' but required in type 'TestEvent'.
So, this type AppEvent cannot have any additional properties. How to make it work? Any help appreciated.
Full code example:
class EventDispatcher {
private listeners: Map<AppEvent, Array<AppEventListener<AppEvent>>>
constructor() {
this.listeners = new Map();
}
public dispatch<T extends AppEvent>(event: T): void {
const listeners = this.listeners.get(event.constructor);
if (listeners) {
listeners.forEach((callback) => {
callback(event);
});
}
}
public addListener<T extends AppEvent>(event: T, listener: AppEventListener<T>): void {
let listeners = this.listeners.get(event);
if (!listeners) {
listeners = [listener];
this.listeners.set(event, listeners);
} else {
listeners.push(listener);
}
}
}
class TestEvent {
public name: string = ''
}
const dispatcher = new EventDispatcher();
const listener = (event: TestEvent) => {
console.dir(event);
};
dispatcher.addListener(TestEvent, listener);
const event = new TestEvent('name');
dispatcher.dispatch(event);
It looks like you should make AppEventLister a generic type referring to a non-generic function, like this:
type AppEventListener<T> = (event: T) => void;
You're saying "an AppEventListener<T> will take an event of type T". That's what you want, right?
Your original definition was a generic type referring to a generic function, with two different generic parameters of the same name, equivalent to
type AppEventListenerBad<T> = <U>(event: U) => void;
(the rename doesn't change the type; it just makes makes more clear what was happening with type parameter name shadowing) meaning "an AppEventListener<T> will ignore T entirely and take an event of any type U that the caller wants to specify", which is unlikely what you mean. And that's where your error is coming from: (event: TestEvent) => void is not assignable to <U>(event: U) => void.
Once you do this, a lot of other issues are solved, but they might bring up other problems. A major one is that the standard library typings for Map represent a very basic mapping from keys of type K to values of type V. It does not have a way to say that keys of certain subtypes of K map to values of programmatically determined subtypes of V. In your case you could make a new interface for the way you are using Map:
interface AppEventListenerMap {
get<T>(x: T): Array<AppEventListener<T>> | undefined;
set<T>(x: T, val: Array<AppEventListener<T>>): void;
}
And then in EventDispatcher you make the listeners property a value of type AppEventListenerMap:
class EventDispatcher {
private listeners: AppEventListenerMap;
constructor() {
this.listeners = new Map();
}
That will mostly make your code example work, except for the fact that you seem to be using event as the key sometimes, and using event.constructor as the key other times. That inconsistency seems wrong, and you probably need to decide exactly the type relationship to get it working properly.
Anyway, hopefully this helps; good luck!
Playground link to code

Categories

Resources