镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Existential type? #14466

Description

Here's a case where I need a few existentials. It's for a definition file, where I need to have parameters for Binding and Scheduler, but it doesn't matter to me what they are. All I care about is that they're internally correct (and any doesn't cover this), and I'd rather not simulate it by making the constructor unnecessarily generic.

The alternative for me is to be able to declare additional constructor-specific type parameters that don't carry to other methods, but existentials would make it easier.

export interface Scheduler<Frame, Idle> {
    nextFrame(func: () => any): Frame;
    cancelFrame(frame: Frame): void;
    nextIdle(func: () => any): Idle;
    cancelIdle(frame: Idle): void;
    nextTick(func: () => any): void;
}

export interface Binding<E> extends Component {
    binding: E;

    patchEnd?(): void;

    patchAdd?(
        prev: string | E | void,
        next: string | E | void,
        pos: number,
    ): void;

    patchRemove?(
        prev: string | E | void,
        next: string | E | void,
        pos: number
    ): void;

    patchChange?(
        oldPrev: string | E | void,
        newPrev: string | E | void,
        oldNext: string | E | void,
        newNext: string | E | void,
        oldPos: number,
        newPos: number
    ): void;
}

export class Subtree {
    constructor(
        onError?: (err: Error) => any,
        scheduler?: type<F, I> Scheduler<F, I>
    );

    // ...
}

export class Root extends Subtree {
    constructor(
        component: type<E> Binding<E>,
        onError?: (err: Error) => any,
        scheduler?: type<F, I> Scheduler<F, I>
    );

    // ...
}

Activity

  1. rotemdan commented on Mar 5, 2017

    @rotemdan

    Hi again :),

    I'll try my best to clarify what this means (hopefully my understanding isn't flawed - at least I hope). I took a simple example as a starting point: of a generic type, and a non-generic container type. The container contains a property content which accepts a value matching any instantiation of the generic type, as long as it follows its internal "shape":

    interface MyType<T> {
    	a: T;
    	b: T[];
    }
    
    interface Container {
    	content<E>: MyType<E>
    }

    Of course the above doesn't currently compile, but it demonstrates another approach to a possible syntax.

    The idea is that the affected property or variable is "modified" by a type parameter, which is always inferred (for the 'write' direction it is conceptually similar to a generic setter method setContent<E>(newContent: MyType<E>) where E is always inferred when called). That type parameter can be passed to any secondary generic type, or even be used to define an anonymous generic type:

    interface Container {
    	content<E>: {
    		a: E;
    		b: E[];
    	}
    }

    One thing to note is that if no constraint is imposed over E. It can always be matched with any, meaning that perhaps surprisingly, the following might work:

    const c: Container = {
    	content: {
    		a: 12,
    		b: ["a", 6, "c"]
    	}
    }

    Since E can be substituted with any, any type with properties a and b and where b is an array can satisfy MyType<E>.

    Even if E was constrained:

    interface Container {
    	content<E extends number | string>: MyType<E>
    }

    The above example can still be matched by E = number | string (Edit: or in practice technically also E = any as well, but I was just giving that as an illustration).

    Maybe what we need is an "exclusive or" type constraint, that would constrain E to be either number or string, but not the possibility of both:

    interface Container {
    	content<E extends number ^ string>: MyType<E>
    }
    const c: Container = {
    	content: {
    		a: 12,
    		b: ["a", 6, "c"]
    	}
    } // <- Error this time: can't match either `number` or `string` to unify with `MyType`

    Another thing to note is that the resulting container type would probably be used mostly as an abstract one, to be inherited by classes or other interfaces, or maybe serve as a constraint for type arguments. When used on its own, trying to read content would return a value whose type always falls back to its existential types' constraints.

  2. dead-claudia commented on Mar 6, 2017

    @dead-claudia
    Author

    Rotem Dan (@rotemdan) I see what you mean. That could work (provided it works for arguments, too). But how would something like this work? (My idea of type<T> Foo<T> similar to Haskell's forall a. Foo a has a similar potential issue, too.)

    interface C {
      prop<T>: Array<T>;
    }
    
    let c: C = {prop: [2]}
    let value = c.prop[0] // What type is this?

    With the information provided, this is an opaque type AFAIK.

  3. rotemdan commented on Mar 6, 2017

    @rotemdan

    @isiahmeadows

    My approach was just an intuitive instinct - a starting point. I can't say I'm an expert in this area but I thought it might be a meaningful contribution.

    I felt it might look simple and aesthetically elegant to modify the identifier itself, though that approach still doesn't cover all possible use cases. It only covers:

    Properties:

    interface C {
      prop<E>: { a: E; b: E[] };
    }

    Variable declaration (const, let and var):

    let c<E>: { a: E; b: E[] }

    Function, method and constructor parameters:

    function doSomething(c<E>: { a: E; b: E[] }, s: string);
    
    interface I {
      doSomething(c<E>: { a: E; b: E[] }, s: string);
    }
    
    class C {
      constructor(c<E>: { a: E; b: E[] }, s: string);
    }

    This syntax doesn't provide a solution to introduce explicit existential-only type variables into other scopes like entire interfaces or classes, or functions. However since these scopes do allow for "universal" type parameters, these type parameters can be "wrapped" by existentials at the use site:

    interface Example<T> {
      a: T;
    }
    
    let x<E>: Example<E>;

    I'm reading about forall in Haskell (I don't think I understand it 100% at this point) and investigating the flexibility of an equivalent type <T> style syntax. I would be interested in seeing more examples for the scopes where type <T> can be used. Can it wrap arbitrary code blocks? can it wrap type aliases? entire constructors, getters and setters, etc?

  4. dead-claudia commented on Mar 6, 2017

    @dead-claudia
    Author

    Rotem Dan (@rotemdan) I'd say the easiest way to understand existentials is through Haskell's version (Haskell/etc. normally leaves it implicit). Basically, it's a lot like Scheduler<any, any>, except it still validates you're implementing Scheduler with the right internal types.

    As a special case, consider polymorphic functions as an existing common special case:

    // TypeScript
    type Flattener = <T>(arg: T[][]) => T[];
    type Flattener = type<T> (arg: T[][]) => T[];
    -- Haskell's equivalent
    type Flattener = [[a]] -> [a]
    type Flattener = forall a. [[a]] -> [a]
  5. rotemdan commented on Mar 6, 2017

    @rotemdan
    • In what code positions can type<T> be used? From your examples so far it seems like it could fit in variable declarations, properties, function and constructor parameters and type aliases. What about entire interfaces, methods, constructors?

    • Since TypeScript has the any type and unions like number | string. How can the compiler decide whether a literal, say { a: 1, b: [1, "x", "y", 4] } is compatible with a polymorphic type, say type <E> { a: E, b: E[] } since E can always be unified with any? (or number | string etc).

    Edit: instantiation -> literal

  6. rotemdan commented on Mar 6, 2017

    @rotemdan

    @isiahmeadows

    I apologize I forgot to answer your question about:

    interface C {
      prop<T>: Array<T>;
    }
    
    let c: C = {prop: [2]}
    let value = c.prop[0] // What type is this?

    When I mentioned that it is "mostly useful in the write direction" I meant that in general it improves type safety only when written to. When read from, the type falls back to a supertype based on the constraint for the existential type parameter (which here is not provided, so I guess can be assumed to be T extends any). So I believe the resulting type of c.prop[0] should be any, unfortunately, unless more sophisticated flow analysis is applied (which might be able to specialize the type only for the particular variable c).

    Based on what I read about 'opaque' types so far, I believe this may qualify as one.

    Edit: my personal feeling about this is that it is just one more example that demonstrates the 'flakiness' of mutable variables. If all TS variables and properties were immutable, the compiler could easily keep track on the 'internal' types held within an entity bound to an existential type, since once it is first inferred, it cannot be changed anymore. Due to this and many other reasons I'm personally making an effort to come up with a plan to try to move away from mutable variables in my own programming.. ASAP :) .

  7. rotemdan commented on Mar 6, 2017

    @rotemdan

    I'll try to demonstrate how this can work with type inference and flow analysis:

    For the type:

    interface C {
      prop<E>: {
        a: E;
        b: E[];
      }
    }

    An instance would provide a temporary instantiation, that would be determined by type inference and flow analysis, for example, when a literal is assigned:

    let x: C = {
      prop: {
        a: 12,
        b: [1, 2, 3]
      }
    }

    The declared type of x would remain C but the actual one would be specialized to { prop: { a: number; b: number[] } } (I'm ignoring, for now, the issues with any inference I mentioned earlier).

    Trying to assign:

    x.prop.b[2] = "hi";

    Should fail, however, reassigning x itself with a value implying a different instantiation might work, e.g.:

    x = {
      prop: {
        a: "hello",
        b: ["world"]
      }
    }

    If x was declared with const then the "apparent" type of x would be permanently locked to { prop: { a: number; b: number[] } }.

    This is at least how I imagine it could work.

    Edit: fixed code examples


    Edit 2:

    After thinking about this for a while, I'm wondering whether prop should be allowed to be reassigned with a value representing a different instantiation as well, e.g.:

    x.prop = {
        a: "hello",
        b: ["world"]
    }

    If that would be allowed (I mean, for both the cases where x was declared as let and const), then flow analysis might need to become more sophisticated as it would need to track the change of type for 'deep' properties in the object tree of x.

  8. zpdDG4gta8XKpMCd commented on Mar 6, 2017

    @zpdDG4gta8XKpMCd

    forgive my ignorance, isn't wrapping into a closure would be a commonly accepted answer to existential types problem?

  9. dead-claudia commented on Mar 6, 2017

    @dead-claudia
    Author

    Not in the case of declaration files, where you almost never see that.

  10. cameron-martin commented on Oct 17, 2017

    @cameron-martin

    @isiahmeadows Surely the type of c.prop[0] in your example would be type<T> T with your proposed syntax?

  11. dead-claudia commented on Oct 17, 2017

    @dead-claudia
    Author

    Cameron Martin (@cameron-martin)

    Surely the type of c.prop[0] in your example would be type<T> T with your proposed syntax?

    That is the correct understanding, but it was Rotem Dan (@rotemdan)'s proposed syntax, not mine - I was just translating a type between the two. The main question I had was this: what is type<T> T equivalent to?

  12. cameron-martin commented on Oct 17, 2017

    @cameron-martin

    @isiahmeadows possibly Object?

  13. dead-claudia commented on Oct 18, 2017

    @dead-claudia
    Author

    Cameron Martin (@cameron-martin) Can't be Object, since Object.create(null), an object by ES spec, should also be assignable to it, and Object.create(null) instanceof Object is false.

  14. 47 remaining items

  15. dead-claudia commented on Aug 15, 2021

    @dead-claudia
    Author

    Asad Saeeduddin (@masaeedu) A better way to put it is that <T>(a: T) => T should be equivalent to forall<T> ((a: T) => T) - the generics should simply be sugar for an enclosing forall constraint. And here's how the math would work out for the previous example per my proposal here:

    Step Action Result
    1 Initial Parameters<<T>(a: T, b: T) => unknown>
    2 Desugar generic to forall Parameters<forall<T> ((a: T, b: T) => unknown)>
    3 Beta reduce forall<T> ((a: T, b: T) => unknown) extends ((...args: infer P) => any) ? P : never
    4 Desugar arguments to tuple forall<T> ((...args: [T, T]) => unknown) extends ((...args: infer P) => any) ? P : never
    5 Lift forall constraint ((...args: forall<T> [T, T]) => unknown) extends ((...args: infer P) => any) ? P : never
    6 Pattern-match type ((...args: forall<T> [T, T]) => unknown) extends ((...args: infer P) => any) ? P : never where infer P = forall<T> [T, T]
    7 Substitute value ((...args: forall<T> [T, T]) => unknown) extends ((...args: forall<T> [T, T]) => unknown) ? forall<T> [T, T] : never
    8 Beta reduce conditional type forall<T> [T, T]

    The current math looks like this:

    Step Action Result
    1 Initial Parameters<<T>(a: T, b: T) => unknown>
    2 Beta reduce (<T>(a: T, b: T) => unknown) extends ((...args: infer P) => any) ? P : never
    3 Resolve generic to supertype ((a: unknown, b: unknown) => unknown) extends ((...args: infer P) => any) ? P : never
    4 Desugar arguments to tuple ((...args: [unknown, unknown]) => unknown) extends ((...args: infer P) => any) ? P : never
    5 Pattern-match type ((...args: [unknown, unknown]) => unknown) extends ((...args: infer P) => any) ? P : never where infer P = [unknown, unknown]
    6 Substitute value ((...args: [unknown, unknown]) => unknown) extends ((...args: [unknown, unknown]) => unknown) ? [unknown, unknown] : never
    7 Beta reduce conditional type [unknown, unknown]

    Note the difference between step 2 of each and the addition of the lifting the forall constraint step - it's subtle, but significant.

    Sorry, it's hard to go into much detail without diving into type theory and mathematical logic. This gets complicated and hairy really, really fast.

    Edit: Correct a couple swapped steps in the current math.

  16. masaeedu commented on Aug 15, 2021

    @masaeedu
    Contributor

    Thanks, that's much more explicit. The "Lift forall constraint" step where you turn this:

    forall<T> ((...args: [T, T]) => unknown) extends ((...args: infer P) => any) ? P : never

    into this:

    ((...args: forall<T> [T, T]) => unknown) extends ((...args: infer P) => any) ? P : never

    is actually wrong. <X> ((x: X) => Whatever) is not the same type as (x: <X> X) => Whatever. The first one is equivalent to (x: unknown) => Whatever, and the second to (x: never) => Whatever.


    As far as I'm aware, your assessment of the "current math" is also wrong, in that the unknowns for the parameters aren't introduced until we try to unify <T>(a: T, b: T) => unknown with (...args: infer P) => any in expanding the conditional type. Or at least, if you write a different conditional type, the quantification survives just fine, and it is possible to show that conditional types respect the upper bounds of quantifiers for generic function types.

    But I'm less confident on this point, probably someone more familiar with the implementation can comment.

  17. dead-claudia commented on Aug 15, 2021

    @dead-claudia
    Author

    Asad Saeeduddin (@masaeedu) I'm not a computer scientist, so I probably didn't nail everything first try. 🙃

    But I will point out one thing that might have gotten missed in the shuffle:

    • forall<X> X is equivalent to the top type unknown.
    • exists<X> X is equivalent to the bottom type never.

    I think you got confused, where I've up until this point only really referred to exists and not forall. (It's also why I started explicitly denoting the two - I'm trying to not be ambiguous in my intent.) So yes, forall<X> ((x: X) => Whatever) is equivalent to (x: forall<X> X) => Whatever and thus also to (x: unknown) => Whatever.

    You are right in that I screwed up in the current math, though, and I've corrected that in my comment.

  18. masaeedu commented on Aug 15, 2021

    @masaeedu
    Contributor

    But I will point out one thing that might have gotten missed in the shuffle:

    • forall<X> X is equivalent to the top type unknown.
    • exists<X> X is equivalent to the bottom type never.

    Unfortunately I don't think this is correct either, it's precisely the other way around. And no, forall<X> ((x: X) => Whatever) is not equivalent to (x: forall<X> X) => Whatever. You're welcome to experiment with this in other type systems with polymorphism (e.g. Haskell), or you could conduct a roughly analogous experiment in TypeScript with <T>(x: () => T) => unknown vs (x: <T>() => T) => unknown.

  19. dead-claudia commented on Aug 15, 2021

    @dead-claudia
    Author

    I think we're both partially, but not fully correct, with you more correct than me.

    declare const test1: Test1; type Test1 = <T>(x: () => T) => unknown
    declare const test2: Test2; type Test2 = (x: <U>() => U) => unknown
    declare const test3: Test3; type Test3 = (x: () => unknown) => unknown
    declare const test4: Test4; type Test4 = (x: () => never) => unknown
    
    // Apparent hierarchy:
    // - Test1/Test3 subtypes Test2/Test4
    // - Test1 is equivalent to Test3
    // - Test2 is equivalent to Test4
    
    // type Test1 = <T>(x: () => T) => unknown
    // type Test2 = (x: <U>() => U) => unknown
    // Result: `Test1` subtypes `Test2`
    const assign_test12: Test2 = test1
    const assign_test21: Test1 = test2 // error
    
    // type Test1 = <T>(x: () => T) => unknown
    // type Test3 = (x: () => unknown) => unknown
    // Result: `Test1` is equivalent to `Test3`
    const assign_test13: Test3 = test1
    const assign_test31: Test1 = test3
    
    // type Test1 = <T>(x: () => T) => unknown
    // type Test4 = (x: () => never) => unknown
    // Result: `Test1` subtypes `Test4`
    const assign_test14: Test4 = test1
    const assign_test41: Test1 = test4 // error
    
    // type Test2 = (x: <U>() => U) => unknown
    // type Test3 = (x: () => unknown) => unknown
    // Result: `Test3` subtypes `Test2`
    const assign_test23: Test3 = test2 // error
    const assign_test32: Test2 = test3
    
    // type Test2 = (x: <U>() => U) => unknown
    // type Test4 = (x: () => never) => unknown
    // Result: `Test2` is equivalent to `Test4`
    const assign_test24: Test4 = test2
    const assign_test42: Test2 = test4
    
    // type Test3 = (x: () => unknown) => unknown
    // type Test4 = (x: () => never) => unknown
    // Result: `Test3` subtypes `Test4`
    const assign_test34: Test4 = test3
    const assign_test43: Test3 = test4 // error

    It appears a reduction for the sake of conditional type matching is possible, but only from Test1/Test3 to Test2/Test4. So my reduction is sound, but I can't go in the other direction - the reduction is one-way, and it can only be done for cases where direct equivalence is irrelevant (like conditional type extraction and assignability).

  20. masaeedu commented on Aug 15, 2021

    @masaeedu
    Contributor

    I think we're both partially, but not fully correct, with you more correct than me.

    Fascinating. Could you point out what I am "not fully correct" about?

  21. dead-claudia commented on Aug 15, 2021

    @dead-claudia
    Author

    Asad Saeeduddin (@masaeedu) I'm specifically referring to the subtyping bit - you're right that they're not equivalent (thus countering almost my entire rationale), but I'm right in that it can still be reduced like that in this particular situation anyways. So essentially, I'm right about the claim it can be done, just you're right about the supporting evidence provided being largely invalid and about why it's largely invalid.

  22. clinuxrulz commented on Oct 21, 2022

    @clinuxrulz

    Workaround:

    type Exists<T> = <R>(doIt: <A>(a: T<A>)=>R)=>R

    That is to say, you represent the type as a callback for the visitor pattern pattern to operate on the unknown type. Unfortunately the pattern requires higher kinded types to avoid boilerplate. So you need to manually make several Exist types for each type of T.

    One solid use case for the exists operator is a Properties List table holding properties of many different types each tupled with a renderer and/or editor that operates on those types.

    E.g.

    type Property<A> = {
      displayName: string,
      getValue: () => A,
      setValue: (a: A) => void,
      renderer: Renderer<A>,
      editor: Editor<A>,
    };
    type PropertyExists = <R>(doIt: <A>(a: Property<A>) => R) => R;
    type PropertyList = PropertyExists[];
    

    100% type-safe to work with. But there is boilerplate.

    Using the workaround above, you can operate on the property list as follows:

    function operateOnPropertyList(pl: PropertyList) {
      for (let p of pl) {
        p(operateOnProperty);
      }
    }
    
    function operateOnProperty<A>(p: Property<A>) {
      // Do stuff with it
    }
    

    Here is a syntax I would suggest if we were to have the exists keyword:
    type PropertyList = (<exists A>: Property<A>)[];

    And the only function types that can type-safely operate on those existential types are the ones using forall.

    E.g.

    function operateOnPropertyList(pl: PropertyList) {
      for (let p of pl) {
        operateOnProperty(p);
      }
    }
    
    function operateOnProperty<A>(p: Property<A>) {
      // Do stuff with it
    }
    
  23. HansBrende commented on Nov 30, 2024

    @HansBrende
    Contributor

    I just ran into this problem. (Apologies if this use-case has already been presented.) Given the following type:

    type KeyObjectPair<OBJ> = readonly [keyof OBJ, OBJ]

    The problem is I need to discard the information about OBJ after such a pair is created, while ensuring that it was type-checked by some OBJ type at creation-time. [Notice that OBJ is invariant in this type signature, otherwise I would be able to simply replace it with unknown or never and call it a day!]

    So that I can do stuff like this later:

    type KeyObjectPairArray = KeyObjectPair<?>[]
    // or:
    type KeyObjectPairArray = (<exists OBJ> KeyObjectPair<OBJ>)[]
    // or whatever the syntax may be
    
    // So that:
    const x: KeyObjectPairArray = [['key', {key: 'value'}], ['k', {k: 'value'}]]  // Works!
    const x: KeyObjectPairArray = [['key', {}]]  // Compile error!

    and functions like this will continue to work later on this "erased" KeyObjectPair:

    function getValue<OBJ>([key, obj]: KeyObjectPair<OBJ>): OBJ[keyof OBJ] {
        return obj[key];
    }
    
    function getValueErased(pair: KeyObjectPair<?>): unknown {
        return getValue(pair) // Works!
    }

    I've looked through the various workarounds posted such as the <R>(doit: <A>(a: MyType<A>) => R) => R hack, however this seems to be very unpleasant to include in a user-facing library.

  24. essenmitsosse commented on Dec 1, 2024

    @essenmitsosse

    Hans Brende (@HansBrende), that is a good use case for existential type and it is currently impossible to represent in TypeScript. There is one workaround, which requires a run time change. You can wrap all of it in a function that returns a callback function which takes callback function that will return the result.

    type KeyObjectPair<OBJ extends Record<string, unknown>> = readonly [
      keyof OBJ,
      OBJ,
    ]
    
    const createKeyObjPairWrapper =
      <OBJ extends Record<string, unknown>>(keyObjPair: KeyObjectPair<OBJ>) =>
      <RESULT>(callback: (obj: KeyObjectPair<OBJ>) => RESULT): RESULT =>
        callback(keyObjPair)
    
    const wrappedKeyObjPair1 = createKeyObjPairWrapper(['a', { a: 1, b: 2 }])
    const wrappedKeyObjPair2 = createKeyObjPairWrapper(['x', { x: 'X', y: 'Y' }])
    
    const wrappedKeyObjPairList = [wrappedKeyObjPair1, wrappedKeyObjPair2]
    
    const getValue = <OBJ extends Record<string, unknown>>([
      key,
      obj,
    ]: KeyObjectPair<OBJ>) => obj[key]
    
    const value1 = wrappedKeyObjPair1(getValue)
    const value2 = wrappedKeyObjPair1(getValue)
    const listResult = wrappedKeyObjPairList.map((callback) => callback(getValue))

    Playground

    Now the wrappers will take any function that takes a KeyObjectPair and does anything to it, that is valid for the generic type it has.

    This might seem unnecessary, but is actually the only way I know how to make this work.

    There is one other solution though, and that is to really ask yourself if you need OBJ to be invariant. In a lot of cases you can actually find a solution, where you can just type your function explicitly. I am not saying this is the case here, but it is more often then not. Props to Moritz Andrich (@MoritzR) for teaching me about this sorcery.

  25. HansBrende commented on Dec 1, 2024

    @HansBrende
    Contributor

    One additional incredible use-case I just thought of! If I'm not mistaken, existential types would allow us to represent INTEGERS in a typesafe way (probably just one of many amazing constructions we could do along these lines).

    Here's how that could happen:

    // Here's what typescript allows so far:
    
    type AssertInt<N extends number> = `${N}` extends `${bigint}` ? N : never;
    
    const assertInt = <N extends number>(n: AssertInt<N>) => n;
    
    assertInt(23);  // Works
    assertInt(23.5);  // Very very cool: compiler error!

    All that is pretty dang cool, but here is the final missing piece via existential types:

    // The final missing piece:
    type Int = AssertInt<?> 
    // i.e., 
    type Int = <exists N extends number> AssertInt<N>
    
    const ints: Int[] = [1, 2, 3, 4]  // valid
    const ints: Int[] = [1, 2, 3, 4.5]  // compiler error

    🧨 💥

  26. barasztamas commented on Aug 17, 2025

    @barasztamas

    I think this is a common use case:

    type MutationButtonProperties<Data> = {
      functionCall: () => Promise<Data | null>
      onSuccess?: (data: Data | null) => void
    }
    
    function createButtons(props: MutationButtonProperties<???>[])

    I understand there are magic solutions for this (e.g. proposed by #14466 (comment)), but I wouldn't want to use advanced level TS sourcery in a simple use case like this

  27. MoritzR commented on Aug 17, 2025

    @MoritzR

    I think this is a common use case:

    type MutationButtonProperties<Data> = {
      functionCall: () => Promise<Data | null>
      onSuccess?: (data: Data | null) => void
    }
    
    function createButtons(props: MutationButtonProperties<???>[])

    I understand there are magic solutions for this (e.g. proposed by #14466 (comment)), but I wouldn't want to use advanced level TS sourcery in a simple use case like this

    For simple use cases like this there is an easier solution, achieved by combining both functions:

    type MutationButtonProperties = () => Promise<void>
    
    function createButtons(props: MutationButtonProperties[])

    If you really need to be able to observe the intermediate null value you can forward it by returning something like Promise<"had data" | "got null"> instead of Promise<void>.

  28. devanshj commented on Jan 19, 2026

    @devanshj

    If anyone is interested in playing with existential types I have a PR open here that you can run locally.

    Here's how #14466 (comment) looks...

    Image

    And here's how #14466 (comment) looks... (although not the intended use case for the PR)

    Image

    Note that the PR is basic and doesn't handle all the cases but feedback is welcome.

  29. irfanstract commented on Aug 30, 2026

    @irfanstract

    any chance the linked PR would get revived?

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions