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

Relating keys and field constraints of a generic super type constructed with mapped type filtering out properties #47447

Open
Igorbek opened this issue Jan 14, 2022 · 2 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@Igorbek
Copy link
Contributor

Igorbek commented Jan 14, 2022

Bug Report

🔎 Search Terms

  • indexing related keys of mapped types
  • supertype mapped type
  • excluding keys mapped type

🕗 Version & Regression Information

  • This is the behavior in every version I tried (4.4, 4.5, 4.6-dev), and I reviewed the FAQ for entries about mapped types

⏯ Playground Link

Playground link with relevant code

💻 Code

// SuperType<T, C> constructs a super type of T by filtering out properties of T that do not extend C

// Option 1 (the most correct, removes keys)
type SuperType<T, C> = { [K in keyof T as (T[K] extends C ? K : never)]: T[K] }

// Option 2 (not correct, resets fields to never)
// type SuperType<T, C> = { [K in keyof T]: T[K] extends C ? T[K] : never }

// Option 3 (Option 1 and 2 combinded)
// type SuperType<T, C> = { [K in keyof T as (T[K] extends C ? K : never)]: T[K] extends C ? T[K] : never }

// Option 4 (I saw this used in the wild)
// type SuperType<T, C> = Pick<T, { [K in keyof T]: T[K] extends C ? K : never }[keyof T]>

type Nums<T> = SuperType<T, number>

// examples
type A = { a: 10, b: true }
type CA = Nums<A>   // Option 1, 3, 4: { a: 10 }
                    // Option 2: { a: 10, b: never }
type CAK = keyof CA // Option 1, 3, 4: 'a'
                    // Option 2: 'a' | 'b'

function f<T, U extends keyof Nums<T>>(t: T, m: U) {
    // This should be reduced to T, because T extends SuperType<T, *>
    type ShouldBeT = T & Nums<T>

    // This should be reduced to U, since it is keysof T extends U
    type ShouldBeU = U & keyof T

    // n1 expected to be bounded to number, because m is keyof Nums<T>
    const n1 = t[m] // Option 1, 2: T[U]
                    // Option 3: any, error Type 'U' cannot be used to index type 'T'.(ts2536)
    n1.toFixed()    // Option 1, 2, 4: bounded to unknown
                    // Option 3: any type

    // same, n1 expected to be bounded to number, but with a hint
    const n2 = t[m] as SuperType<T, number>[U] // Option 1, 2: ok
                                               // Option 3: Type 'U' cannot be used to index type 'T'.(2536)
    n2.toFixed()    // Option 1, 4: bounded to unknown
                    // Option 2: bounded to number (success!)
                    // Option 3: bounded to number (because of type assertion)
}

🙁 Actual behavior

  • TS cannot relate a key that is a key of a supertype (with same or lesser keys) constructed by mapped type removing some keys
  • TS cannot index a generic type with a key of its generic supertype

🙂 Expected behavior

  • TS can relate a key that is a key of a supertype (with same or lesser keys) constructed by mapped type removing some keys
  • TS can index a generic type with a key of its generic supertype

Reasoning

  • any field with key U of SuperType<T, C> is also a key of T and its type extends C
  • therefore T[U] should be allowed because U is keyof T
  • therefore T[U] should be C because T[K] extends C for any key in U

That is a very common task that you need to filter out some properties from a type. I saw people call it a subtype when in fact it is a supertype. But weirdly enough indexing be keys of the mapped type is not allowed. I saw it as a regression in a big codebase when upgraded to 4.5, but turned out the values were considered any. So now they're bounded to unknown which is better but still cannot do much further.

@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Jan 18, 2022
@RyanCavanaugh
Copy link
Member

I'm not sure what to do here. How could TS identify the type relationship between T and { [K in keyof T as (T[K] extends C ? K : never)]: T[K] }, or any other complex mapped type operation that produces a supertype by construction? The "Reasoning" presented here is sound, but we can't fire up a proof solver during compile-time every time we need to relate two types and see if something happens.

This has come up before in other contexts and it seems like we need a type operator that says "Filter T's properties based on their types' relation to U. That operator could plausibly have higher-order behavior since it would presumably be able to guarantee the linearity of T to pick U from T or whatever it would be. The other alternative would be to introduce some explicit linearity syntax to generic type declarations where you could (without verification) make assertions about the variance relation between type G<T, U> and its output, e.g. maybe something like

// (syntax intentionally awful to avoid anchoring)
type G<T, U> i declare that it is super T = ...` 

@jcalz
Copy link
Contributor

jcalz commented Nov 29, 2023

cross-linking to #48992

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants