Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exact generic constraints #35899

Open
5 tasks done
athyuttamre opened this issue Dec 29, 2019 · 12 comments
Open
5 tasks done

Exact generic constraints #35899

athyuttamre opened this issue Dec 29, 2019 · 12 comments
Labels
Discussion Issues which may not have code impact

Comments

@athyuttamre
Copy link

athyuttamre commented Dec 29, 2019

Search Terms

  • Generics
  • Constraints
  • HOCs
  • Injected types

Suggestion

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: #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.

Use Cases

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.

Examples

As above.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
    • In particular, this helps make TypeScript more composable, as per "Produce a language that is composable and easy to reason about.".
@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 29, 2019

Why use generics when you are not using the type parameter?

const hoc = (child: (arg: {a: string}) => void): void => {
  child({ a: 'apple' });
};

@athyuttamre
Copy link
Author

athyuttamre commented Dec 29, 2019

@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.

@AnyhowStep
Copy link
Contributor

I've got a workaround but it isn't the prettiest,
Playground

@jack-williams
Copy link
Collaborator

Your solution probably sits somewhere between #14520 and #12936

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 29, 2019

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

@jack-williams
Copy link
Collaborator

{} 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

@Gerrit0
Copy link
Contributor

Gerrit0 commented Dec 29, 2019

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

@janderson7118
Copy link

janderson7118 commented Dec 29, 2019 via email

@jack-williams
Copy link
Collaborator

jack-williams commented Dec 29, 2019

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}) => {}.

@RyanCavanaugh RyanCavanaugh added the Discussion Issues which may not have code impact label Jan 14, 2020
@RyanCavanaugh
Copy link
Member

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

@RobertAKARobin
Copy link

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.

@Bielik20
Copy link

I have similar but slightly more complex issue: #41930

I think I might have solved your problem while working on it. Take a look at my example and I must say it works as "extending" middleware. So that you can make it so:

middleware(foo(), bar(), handler);

foo and bar may add something to payload while bar and handler may require some types. There are only 3 files there middleware.ts, middleware.spec.ts utils.ts so it should be easy for you to copy and test it for yourself. For now I implemented types for only up to 3 functions but it can be extended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests

8 participants