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

Simulating supertype constraints #29046

Closed
OliverJAsh opened this issue Dec 15, 2018 · 2 comments
Closed

Simulating supertype constraints #29046

OliverJAsh opened this issue Dec 15, 2018 · 2 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Dec 15, 2018

TypeScript Version: 3.2.2

Search Terms: supertype constraint extends partial

Code

I am trying to write a higher-order function that applies default parameters to a function that has "named parameters". Currently I have the following code:

type Omit<A extends object, K extends string | number | symbol> = Pick<A, Exclude<keyof A, K>>;
type Diff<A extends object, K extends keyof A> = Omit<A, K> & Partial<Pick<A, K>>;

const withDefaults = <
    Params extends object,
    DefaultParamsKeys extends keyof Params,
    Result
>(
    fn: (params: Params) => Result,
    defaultParams: Pick<Params, DefaultParamsKeys>,
) => (inputParams: Diff<Params, DefaultParamsKeys>): Result =>
    // Cast necessary to workaround https://github.com/Microsoft/TypeScript/issues/28748
    fn(({
        ...defaultParams,
        ...inputParams,
    } as unknown) as Params);

// Example

{
    type Params = { a: string; b: number; c?: string; };
    const _fn = (params: Params) => {};

    withDefaults(_fn, {
        b: '1', // expect error: defaults are type checked
    });

    const defaults = {
        b: 1,
        c: 'foo'
    };
    const fn = withDefaults(_fn, defaults);
    fn({}); // expect error: non-defaults are required
    fn({ a: 1 }); // expect error: non-defaults are type checked
    fn({ a: 'foo' }); // defaults may be emitted
    fn({ a: 'foo', b: 2 }); // defaults may be overridden
    fn({ a: 'foo', b: '' }); // expect error: defaults are type checked
}

This works, but I'm wondering if it could be made easier.

If I understand correctly, we want to enforce that the defaultParams parameter is a supertype of the Params generic.

I'm achieving this in a semi-roundabout way using extends keyof and Pick:

// Constrain `defaultParams` to supertype of `Params`

const fn = <
    Params extends object,
    DefaultParamsKeys extends keyof Params
>(
    params: Params,
    defaultParams: Pick<Params, DefaultParamsKeys>,
) => {
    defaultParams = params;

    declare let paramsKey: keyof Params;
    declare let defaultParamsKey: DefaultParamsKeys;

    paramsKey = defaultParamsKey
}

I had expected to achieve the same effect using extends Partial<Params>, but it seems not:

const fn2 = <
    Params extends object,
    DefaultParams extends Partial<Params>
>(
    params: Params,
    defaultParams: DefaultParams,
) => {
    // Type 'Params' is not assignable to type 'DefaultParams'.
    defaultParams = params;

    declare let paramsKey: keyof Params;
    declare let defaultParamsKey: keyof DefaultParams;

    // Type 'keyof DefaultParams' is not assignable to type 'keyof Params'.
    paramsKey = defaultParamsKey
}

Is this a bug or intended behaviour?

Ideally TypeScript would have syntax to make this easier:

const fn = <
    Params extends object,
    DefaultParams extended by Params
>(
    params: Params,
    defaultParams: DefaultParams,
) => {
    // …
}

I believe syntax like this was initially suggested in #9252 and #7265, although Partial types claimed to be the solution, even though I can't get it to work 🤔

Note I may have got the wrong idea about defaultParams needing to be a supertype of Params? 🤔

Potentially related to #14520 also.

@weswigham weswigham added the Question An issue which isn't directly actionable in code label Dec 17, 2018
@OliverJAsh
Copy link
Contributor Author

Hi @weswigham, do you have any information relating to this?

@OliverJAsh
Copy link
Contributor Author

This is not possible with

<DefaultParams extends object, Params extends DefaultParams, Result>

because DefaultParams can sometimes be a subtype (more specific) of Params. E.g. from the example above, when providing defaults for an optional key c, DefaultParams will be { c: string; } whereas Params will be { c?: string; }.

I'm now thinking that DefaultParams should be neither a supertype of subtype of Params. It is just a picked version of Params? In which case, my solution above would be the way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

2 participants