Skip to content

Reusable callbacks: When destructuring, missing properties should be excluded from the infered type.Β #63643

Description

@denis-migdal

πŸ” Search Terms

  • destructured function parameters.
  • contextual typing (?).

Not sure if I have the correct vocabulary.

Related issues :

βœ… Viability Checklist

⭐ Suggestion

When inferring the type of a ({arg1, arg2}) => void callback, missing properties should be excluded from the inferred type.

Currently, TS infers it as ({arg1, arg2}: T) => void, with T being the constraint used during the inference.
I'd like the inferred type to be : ({arg1, arg2}: Pick<T, "arg1"|"arg2">) => void.

TS is able to properly infer the callback type when the whole argument is missing (i.e. () => void), so I guess my suggestion should also be technically possible ?

πŸ“ƒ Motivating Example

This can be useful to declare callbacks that must be compatible with a given context, while enabling their usage in other contexts, compatibles with the callbacks, not necessary with the original context.

Playground Link

type Bindings<CTX extends Record<string, any>> = {
    [K in keyof CTX]: (ctx: CTX) => void; // <- in reality the type here is a little more complex.
}

// in reality we use B & Record<Exclude<keyof B, keyof T>, never> to exclude extra-keys.
function foo<T extends Record<string, any>, B extends Bindings<NoInfer<T>>>(p: {props: T, bindings: B}) {
    return p;
}

const x = foo({
    props: {
        foo: 34, faa: "ok", fuu: "ok", fii: 42
    },
    bindings: {
        fii:() => {},                   // OK: () => void
        foo:(a) => {},                  // OK: (a: T) => void
        faa:(args: {foo:number}) => {}, // OK: ({foo: number}) => void
        fuu: ({foo}) => {}              // NOK: ({foo}: T) => void / we want ({foo: number}) => void
    }
})

// we could reuse it like :
foo({
    props: {
        ...x.props,
        fii: string // override a property with a completely different type.
    },
    bindings: {
       ...x.bindings,
       fii: () => {} // binding may need to be overridden too.
    }
})

πŸ’» Use Cases

  1. What do you want to use this for?

Writing reusable callbacks.

  1. What shortcomings exist with current approaches?
  2. What workarounds are you using in the meantime?

We need to explicit the types when destructuring, e.g. faa: (args: {foo:number}) => {}).

A possible workaround could have been the following, however TS doesn't do a "per key" inference.

type Bindings<CTX extends Record<string, any>, KEYS extends Record<keyof CTX, keyof CTX>> = {
    [K in keyof CTX]: (ctx: Pick<CTX, KEYS[K]>) => void; // <- in reality the type here is a little more complex.
}

An alternative solution would be to allow TS to be more lax with ({foo}: T) => void, e.g. allowing ({foo: ...}) calls, even if the argument doesn't respect the T constrainst.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions