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.

never doesn't equal never when never is keys/values from filtering generic object wrapped inside another object by exact value

See original GitHub issue

Bug Report

🔎 Search Terms

never equals filter mapped type

🕗 Version & Regression Information

It’s broken in all tested versions of typescript (4.1.5 until 4.9.0-dev.20221011 in TS playground). Versions prior 4.1.5 do not support mapped types keys filtering.

⏯ Playground Link

Playground minimal code

Playground both keyof and of Values<> cases

💻 Code

So wee need to:

  • Wrap generic argument, lets say O, in an object(i.e {anyKey: O} or just [O]).
  • Then filter object keys by exact value via Equals<> in such a way to pass further empty object.
  • Extract keys or values. Since object is empty never will be passed.
  • And finally compare it with never via Equals<>
// helpers start

type Values<O extends object> =
  O extends any[] 
    ? O[number]
    : O[keyof O]
  
type Equals<A, B> = 
  (<T>() => T extends B ? 1 : 0) extends 
  (<T>() => T extends A ? 1 : 0)
    ? true
    : false

// helpers end

type FilterByStringValue<O extends object> = {
  [K in keyof O as Equals<O[K], string> extends true ? K : never]: any
}

type FilteredValuesMatchNever<O extends object>
  = Equals</*never*/Values</*{}*/FilterByStringValue<[O]>>, never>

type filteredValuesMatchNever = FilteredValuesMatchNever<[]> // false wrong

🙁 Actual behavior

filteredValuesMatchNever is false

🙂 Expected behavior

filteredValuesMatchNever must be true. Actually with the FilterByStringValue rewritten like this:

type FilterByStringValue<O extends object> = {
  [K in keyof O as Equals<O[K], string> extends true ? K : never]: O[K]
}

filteredValuesMatchNever somehow becomes true

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:1
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

3reactions
Andaristcommented, Oct 16, 2022

This is quite interesting!

We have 3 different Equals implementations here, let’s call them as follows:

  • nested: type Equals<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false;
  • short: type Equals<A, B> = [A, B] extends [B, A] ? true : false;
  • identity-based: type Equals<A, B> = (<T>() => T extends B ? 1 : 0) extends (<T>() => T extends A ? 1 : 0) ? true : false

We also have 2 different implementations of the filtering mapped type:

  • any-value: one with any at the value position of the mapped type, note that different types at this position can also exhibit the same, broken, behavior
  • indexed access value: one with O[K] at the value position of the mapped type

I don’t intend to explore all combinations of those 2 sets but I will mention particular variants from both of those.

nested Equals + any-value

This one works correctly: TS playground

  1. this one is recognized as “typical nondistributive conditional type” (here)
  2. so the type parameter gets unwrapped from the single-element tuple in the check type (here)
  3. and thus the check type is classified as instantiatable (here)
  4. based on this TypeScript doesn’t attempt to resolve the conditional type (here). Thanks to that the conditional type is left roughly untouched and gets finally resolved when the actual type arguments are supplied to it

short Equals + any value

This one doesn’t work correctly: TS playground

  1. this isn’t recognized as the typical nondistributive conditional type
  2. its check type is left unwrapped
  3. its instantiated check type is [A, B] and it’s not classified as generic here. It turns out that a generic tuple type is only a one with a variadic element (isGenericTupleType)
  4. So the TypeScript attempt to resolve this based on the check here
  5. Resolving involves checking the assignability of the permissive intantiations of checkType ([any, never]) and inferredExtendsType ([never, any])
  6. since any is not assignable to never this assignability check fails and falseType gets selected as the result (here)

short Equals + indexed access value

This one works correctly: TS playground

It’s kinda interesting because almost everything here is the same as in the variant described above. However, in here checking the assignability of the permissive intantiations returns a different result despite the fact that we seamingly compare the same types ([any, never] and [never, any]).

So why is that? In here, the any type is actually not the “true any” - it’s a “wildcard type” and that is assignable to never. So based on that the overall conditional type stays roughly untouched and gets finally resolved when the actual type arguments are supplied to it.

Note: it would be kinda nice if the wildcard type would get printed as any* or something like that. The same could be done about the silentNeverType (and maybe some others). Since they are printed in the same way, it’s fairly easy to not notice the difference between them when debugging.

identity-based Equals + any value

This one doesn’t work correctly: TS playground

This is fairly similar to the “short Equals + any value” case. The only difference here is what permissive instantiations we are comparing here for checkType (<T>() => T extends never ? 1 : 0) and inferredExtendsType (<T>() => T extends any ? 1 : 0). Since those are not assignable, we simply select the falseType here too.

0reactions
RyanCavanaughcommented, Oct 13, 2022

The fact that it still repros on the tuple form is pretty interesting, since that should be going through the normal relational path. Though I wonder if the first thing that gets checked is the index signature between the two types.

Read more comments on GitHub >

github_iconTop Results From Across the Web

What is "not assignable to parameter of type never" error in ...
So when you tried to add a string to it, it was a type mismatch, and so it threw the error you saw....
Read more >
Documentation - Narrowing - TypeScript
JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript takes this into account...
Read more >
A complete guide to TypeScript's never type - Zhenghao
Filter out keys in mapped types#. In TypeScript, types are immutable. If we want to delete a property from an object type, we...
Read more >
Value Conversions - EF Core - Microsoft Learn
Configuring value converters in an Entity Framework Core model. ... A null value will never be passed to a value converter. A null...
Read more >
jq Manual (development version)
Instead of running the filter for each JSON object in the input, read the entire ... was either false or null , or...
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