Skip to content

Conversation

@devanshj
Copy link

@devanshj devanshj commented Dec 30, 2025

Imagine you have a function consumeLayer...

type Layer<T extends object> = {
  children: T[],
  key: (child: T) => string
}

declare const consumeLayer: 
  <T extends object>(layer: Layer<T>) => void

consumeLayer({
  children: [
    { name: "Alpha" },
    { name: "Beta" }
  ],
  key: child => child.name
})

Okay good. How what if we need to consume two layers? We can have another parameter but then we'll also need another type parameter because the children of those two layers might be of different shapes. So it'd look like this...

type Layer<T extends object> = {
  children: T[],
  key: (child: T) => string
}

declare const consumeLayers: 
  <T1 extends object, T2 extends object>
    (layer1: Layer<T1>, layer2: Layer<T2>) => void

consumeLayers(
  {
    children: [
      { name: "Alpha" },
      { name: "Beta" }
    ],
    key: child => child.name
  },
  {
    children: [
      { size: 4 },
      { size: 5 }
    ],
    key: child => child.size.toString()
  }
)

Okay so far so good. Now what if we had to accept an array of layers? How do we dynamically create "n" generics? Surely one generic won't make the cut because each layer can have a different shape of children and having one generic will squash those child shapes into a union...

type Layer<T extends object> = {
  children: T[],
  key: (child: T) => string
}

declare const consumeLayers: 
  <T extends object>(layers: Layer<T>[]) => void

consumeLayers([
  {
    children: [
      { name: "Alpha" },
      { name: "Beta" }
    ],
    key: child => child.name
//       ^? { name: string; size?: undefined; } | { size: number; name?: undefined; }
//                ~~~~~~~~~~
//                Type 'string | undefined' is not assignable to type 'string'.
//                  Type 'undefined' is not assignable to type 'string'.(2322)
  {
    children: [
      { size: 4 },
      { size: 5 }
    ],
    key: child => child.size.toString()
//       ^? { name: string; size?: undefined; } | { size: number; name?: undefined; }
//                ~~~~~~~~~~
//                'child.size' is possibly 'undefined'.
  }
])

How do we solve this?

The answer is we can't. (Unless we employ some hacky workarounds that end up reducing type safety). But let's take a step back and think... Do we even need the generic on the function? Surely the layer type needs a type parameter to express it's structure but does that have to be on the function? What if TypeScript allowed us to express the structure like this...

type Layer = <T extends object> {
  children: T[],
  key: (child: T) => string
}

If this was possible we could simply type the layers as Layer[] and call it a day...

type Layer = <T extends object> {
  children: T[],
  key: (child: T) => string
}

declare const consumeLayers: 
  (layers: Layer[]) => void

consumeLayers([
  {
    children: [
      { name: "Alpha" },
      { name: "Beta" }
    ],
    key: child => child.name
  {
    children: [
      { size: 4 },
      { size: 5 }
    ],
    key: child => child.size.toString()
  }
])

This is precisely what this PR enables... You can create a type parameter out of thin air and have a richer way to express your types. Formally such types are called universally quantified types.

As a matter of fact this also unintentionally allows you to create "Self-Type-Checking" types... For example here's the CaseInsensitive type...

type PasreCaseInsensitive<Self, T extends string> =
  Self extends string
    ? Lowercase<Self> extends Lowercase<T>
        ? Self
        : `Error: Type '${Self}' is not assignable to type 'CaseInsensitive<${T}>'`
    : T

type CaseInsensitive<T extends string> = <Self> PasreCaseInsensitive<Self, T>

declare const setHeader: 
  (key: CaseInsensitive<"Set-Cookie" | "Accept">, value: string) => void

setHeader("Set-Cookie", "test") // compiles
setHeader("Accept", "test2") // compiles
setHeader("sEt-cOoKiE", "stop writing headers like this but ok") // compiles
setHeader("Acept", "nah this has a typo") // does not compile
//        ~~~~~~~
// Argument of type '"Acept"' is not assignable to parameter of type '"Error: Type 'Acept' is not assignable to type 'CaseInsensitive<Accept>'" | "Error: Type 'Acept' is not assignable to type 'CaseInsensitive<Set-Cookie>'"'.ts(2345)

Any self-type-checking type can be expressed as a quantified type, but not the other way round. And quantified types being superset of self-type-checking types is totally unintentional, I did not want to implement self-type-checking types again haha.

This consumeLayers problem is something I actually encountered while working at something else and I thought it would be fun to implement quantified types in typescript. So this PR is just an experiment and doesn't intend to be merged in (nor does it handle all edge cases). So feel free to close it. But I'd love to hear what thoughts y'all have on this. I'd also love to see what other use-cases people can come up with.

Thanks for reading.

@gabritto
Copy link
Member

This seems like a duplicate of microsoft/TypeScript#14466.

@devanshj
Copy link
Author

devanshj commented Dec 30, 2025

This seems like a duplicate of microsoft/TypeScript#14466.

Yes this PR implements a quantified type! Should have linked this issue in the PR description but forgot that there's an issue for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants