question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Exact generic constraints

See original GitHub issue

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

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

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:4
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

3reactions
jack-williamscommented, Dec 29, 2019

Your solution probably sits somewhere between #14520 and #12936

1reaction
Gerrit0commented, 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
Read more comments on GitHub >

github_iconTop Results From Across the Web

Exact type constraints in generic types - C# - Stack Overflow
I want to implement a generic type mapper and I'm struggling with a problem relating to generic type constraints. I have and entity...
Read more >
Narrow Down Types With Typescript Generic Constraints
In Typescript, this is called Generic Constraint. ... a function that accepts a custom shape and return the exact same shape like this,...
Read more >
Using generic type constraints in Swift | Swift by Sundell
Using generic type constraints, we are able to add new APIs and behaviors to a type only under a certain set of constraints....
Read more >
Generic Types - Visual Basic | Microsoft Learn
A constraint can require that the type argument must implement a particular interface, be or inherit from a particular class, have an accessible ......
Read more >
How To Use Generics in TypeScript - DigitalOcean
result has the type 123 , which is the exact number that you passed in. ... extends Record<string, any> is known as generic...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found