Sitelet https://web.archive.org/web/20220621124307/https://github.com/microsoft/TypeScript/issues/41173
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

Accept de-structured elements in type predicates #41173

Open
5 tasks done
rraziel opened this issue Oct 20, 2020 · 5 comments
Open
5 tasks done

Accept de-structured elements in type predicates #41173

rraziel opened this issue Oct 20, 2020 · 5 comments
Labels
Awaiting More Feedback Suggestion

Comments

@rraziel
Copy link

@rraziel rraziel commented Oct 20, 2020 •

Search Terms

  • type predicate
  • reference binding pattern
  • type predicate cannot reference
  • destructured

Suggestion

The possibility to use destructured parameters in type predicates.

Use Cases

Destructuring is heavily used in functional/reactive programming, notably with rxjs where various contextual properties tend to be passed between each operator.

Having the ability to succinctly test for types would make the code more readable, e.g.:

type Example = {
  a: number;
  b: string | undefined;
};

const example: Example = {
  a: 42,
  b: 'hello';
};

of(example).pipe(
  guard(({ b }): b is string => b !== undefined, 'b cannot be undefined'),
  tap({ b }) => { /* b is now a string rather than a string | undefined })
);

Right now the alternative is

of(example).pipe(
  guard((x): x is Omit<typeof x, 'b'> & { b: string } => x.b !== undefined, 'b cannot be undefined'),
  tap({ b }) => { /* b is now a string rather than a string | undefined })
);

Or, without a predicate

of(example).pipe(
  map(x => {
    if (x.b === undefined) {
      throw new Error();
    }

    return x;
  }),
  tap({ b }) => { /* b is now a string rather than a string | undefined })
);

Examples

function assertSomething(
  { property }: T
): property is AssertionHere {
  return true;
}

This would roughly translate to something like:

function assertSomething(
  obj: T
): obj is Omit<T, 'property'> & { property: AssertionHere } {
  return true;
}

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.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback Suggestion labels Nov 3, 2020
@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Nov 3, 2020

Self-contained examples that don't assume importing/knowledge of rxjs would be very helpful

@manbearwiz
Copy link

@manbearwiz manbearwiz commented Nov 20, 2020 •

I run into this when filtering the output of Object.entries. I find it much more readable to be able to reference key and value instead of pair[0] and pair[1]. Simplified example but demonstrates a use case outside of rxjs.

If I want all pairs from the query params that are arrays, I currently have to do:

const queryParams = {text: 'foo', statuses: ['status1', 'status2'], regions: []}

Object.entries(queryParams)
 .filter((pair): pair is [string, string[]] => Array.isArray(pair[1]))

or

Object.entries(queryParams)
  .filter(([_, value]) => Array.isArray(value))
  .map(pair => pair as [string, string[]])

I would prefer to do:

Object.entries(queryParams )
  .filter(([_, value]): value is string[] => Array.isArray(value))

@rraziel
Copy link
Author

@rraziel rraziel commented Dec 1, 2020

A simple-with-no-external-elements example could be:

type X = { value: number | string; };
const xs: Array<X> = [{ value: 42 }, { value: 'hello' }];

// without the feature
const filtered = xs
  .filter(({ value }) => typeof value === 'number')
  .map(x => x as { value: number })
;

// with the feature
const filtered = xs
  .filter(({ value }): value is number => typeof value === 'number')
;

@iBlueDust
Copy link

@iBlueDust iBlueDust commented Jul 29, 2021 •

Ran into this problem in React.

I have a React context in the form of a class.
Unfortunately, trying something like

function assertSomething(
  obj: T
): obj is Omit<T, 'property'> & { property: AssertionHere } {
  return true;
}

in my project as suggested by @rraziel doesn't work.

Turns out, since Omit<SomeClass, 'someProperty'> throws all class methods away, intellisense rejects it as incompatible with SomeClass.

In my project, intellisense reported

Type 'Omit<RoomStore, 'room'> & { room: Room; }' is missing the following properties from type 'RoomStore': client, socket, setUser, setRoom, and 10 more.

In conclusion, it'll be real nice for this feature to be implemented (or this bug to be fixed).

@artu-ole
Copy link

@artu-ole artu-ole commented Oct 5, 2021

I agree that this is a useful feature and an unfortunate oversight on typescript's part.
I would, however, note that @rraziel's workaround example can be slightly better versed by not causing an additional loop with map and using destructuring in the filter body to preserve readability.

const filtered = xs
  .filter((x): x is { value: number } => {
    const { value } = x;
    return typeof value === 'number';
  });

Same goes for array destructuring(my use case which involved rxjs's combineLatest)

type X = [ number | string ];
const xs: Array<X> = [[ 42 ], [ 'hello' ]];

// without the feature
const filtered = xs
  .filter((x) => {
    const [ value ] = x;
    return typeof value === 'number';
  })
;

// with the feature
const filtered = xs
  .filter(([ value ]): value is number => typeof value === 'number')
;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback Suggestion
Projects
None yet
Development

No branches or pull requests

5 participants