I would like to write a function that wraps a child function and provides a subset of its required arguments.
Here's an example: https://www.typescriptlang.org/play/index.html#code/PTAEGMAsEsBsBMAUBKUAnApgRwK7UwM6jwCGALiaNAHbiw7w0DmoABia6CdfGwEasAdACgAZjlploAe2oQYCRKQoAuUAG8uagmTTMANKD7bdzUAF9kagG7TovdcNDOIsgtNgZBsaUyXkSQRJDZUC+ZGFzAG5hYRBQSGlwFFACHAAHdNhoDCJ2VkNuXkwyHDRqIkpxSRk5MkhyUFlYAE90bDxCfiFhMhb0jFAAQXhGKVlQAF4NLVTTahZosQlwcblE8AAeABVQDAAPMgweIhGx2oA+RCg4eDV-VVBt1EmL0Ft7K1AHkjUAeQAttAyDtDABrDAtaSiYajYGXF5vD4OJwuaqrWqgADuaBImQwSFC-yBIO24Mh0Nh51kF1QjhcDPkt0QmkEbNChTUAHI8VkMFyLMgYgzzLEGSUynIcbyCTFRcJwG4yNjcfjeNMNtcFPAhcJpWqWUZuXxpC0BZYokA.
However, this errors out because the child function's generic argument H could be defined to be _more_ specific than just a: string (for example, it could be defined to be a: 'foo' | 'bar'). This is covered here: https://github.com/Microsoft/TypeScript/issues/29049, and explained here: https://stackoverflow.com/a/56701587.
It would be useful to have a way to set a constraint on the generic H where it _must_ contain exactly the fields that it is "extend"ing from. For example:
// Note that `contains` here is a made up new term. We can choose another name if preferable.
const hoc = <T contains {a: string}>(child: (arg: T) => void): (arg: Omit<T, 'a'>) => void => {
const wrapper = (arg: Omit<T, 'a'>) => {
child({ ...arg, a: 'apple' });
};
return wrapped;
};
hoc((arg: {}) => {}); // Error, arg does not contain {a: string}
hoc((arg: {a: string}) => {}); // Works!
hoc((arg: {a: string, b: string}) => {}); // Works!
hoc((arg: {a: 'foo' | 'bar'}) => {}); // Error, a must be `string`, not `'foo' | 'bar'`.
I don't fully understand why, but a similar example in Flow works without issue (possibly due to the use of exact types?): https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVxhgMYAsCWMAJgBQCUYATgKYCOArvjQM5hECGALu2PgHbYY9IvwDmYAAbsJYdnyKSARhIB0qKPQGd8cPjgLESHbgC4wAbwA+ss806UxAGjCLb9sWEsBfMmYBucPgK5qhgYTi6zHAw1CrwokZc7CrszsbJimSoXgDc6JhguHDY5GDM9AAOFTD41KxSEs5yCjSc9JR8rDwaWjp6nLhcYLowAJ5UdIwsSqrqmtjauoXFADwAKmbmNmXufOJeAHwkeIREZommYGsUALwHYAFBvmAX7GYAJAAi+FBQ685WbZ2Bx7TyHW73R7BULhHoLPpgBCUdhVaikdIfb6-f4WaxvHYg8TeA4UELhcn6U4kLYqWnpJpmADkKOq1EZYB8eXJXnQ5Na7T0SJZaLyPNQ2EinERyNRChuyxKJ2IZDyQtl1NcYEZijgo0ZnKAA.
My motivating use case is building a well-typed middleware framework. Middleware often add fields to the context of an HTTP request before or after the handler is called. I would like for a Middleware function to exactly specify which fields it adds to the handler. Applied to the playground code above, child is the HTTP handler, and hoc is one such middleware function.
As above.
My suggestion meets these guidelines:
Why use generics when you are not using the type parameter?
const hoc = (child: (arg: {a: string}) => void): void => {
child({ a: 'apple' });
};
@AnyhowStep The code sample was a reduced example. If you click through to the playground, it shows a more meaningful example where the type parameter is being used.
Update: I updated the code sample to include use of the type parameter.
I've got a workaround but it isn't the prettiest,
Playground
Your solution probably sits somewhere between #14520 and #12936
Re: #14520
{} is a super type of { a : string }. So, <T super Addition> wouldn't work for this case.
Re: #12936
Most people think of "exact types" as having exact properties, but not exact types for values.
So, { a : "hi" } would still be assignable to Exact<{ a : string }>
{} is a super type of { a : string }. So,
wouldn't work for this case
You can have upper and lower bound constraints on a parameter
Alternative solution, still requires a cast unfortunately. Playground
// child() requires data including `a` and `b`.
function child(data: { a: string, b: string }): void {
console.log(data.a, data.b)
};
// Merge two objects, taking types from U if possible, and falling back to T
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U ? U[K] : K extends keyof T ? T[K] : never
}
// Remove props in T that are also in U
type RemoveCommonProps<T, U> = Pick<T, Exclude<keyof T, keyof U>>
// hoc() supplies `a`, and returns a function that only requires `b`.
type Addition = { a: string };
function hoc<T extends Addition>(child: (data: Merge<T, Addition>) => void) {
function wrapped(data: RemoveCommonProps<T, Addition>) {
// Still requires a cast unfortunately
child({ ...data, a: 'apple' } as Merge<T, Addition>);
}
return wrapped;
}
const wrapped = hoc(child);
wrapped({ b: 'boy' });
wrapped({ b: 'boy', a: 'a' }) // Error, provided by hoc (freshness check)
const data = { a: 'a', b: 'b' }
wrapped(data) // No error because object literal is not fresh
35899
On Sun, Dec 29, 2019, 11:37 AM Gerrit Birkeland notifications@github.com
wrote:
// child() requires data including
aandb.function child(data: { a: string, b: string }): void {
console.log(data.a, data.b)
};
// Merge two objects, taking types from U if possible, and falling back to Ttype Merge= {
[K in keyof T | keyof U]: K extends keyof U ? U[K] : K extends keyof T ? T[K] : never
}
// Remove props in T that are also in Utype RemoveCommonProps= Pick >
// hoc() suppliesa, and returns a function that only requiresb.type Addition = { a: string };
function hoc(child: (data: Merge ) => void) {
function wrapped(data: RemoveCommonProps) {
// Still requires a cast unfortunately
child({ ...data, a: 'apple' } as Merge);
}return wrapped;}
const wrapped = hoc(child);
wrapped({ b: 'boy' });wrapped({ b: 'boy', a: 'a' }) // Error, provided by hoc (freshness check)
const data = { a: 'a', b: 'b' }wrapped(data) // No error because object literal is not freshβ
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/35899?email_source=notifications&email_token=AAXC6EL3FQWB7WVUBOVQQUDQ3DG4RA5CNFSM4KAZBLTKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEHZDJVY#issuecomment-569521367,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAXC6ELJOFNJLW75SV2QXVLQ3DG4RANCNFSM4KAZBLTA
.
I think @Gerrit0's solution is reasonable. If I understand correctly the errors (or lack-of) at the call to wrapped are less important because the spread overrides correctly.
Expecting an error for a call like hoc((arg: {}) => {}); might not be right because a function of that type can be safely used at types with more specific arguments like (arg: {a: string}) => {}.
Tagging this Discussion since I think there are a variety of good approaches on the table already, plus various other proposals linked to would cover this if implemented
I ran into this same issue, posted on SO, and was very surprised that the response was there's no way to require an exact match. +1 for this feature.
Most helpful comment
Your solution probably sits somewhere between #14520 and #12936