Repository navigation
Type predicate expressions and 'this' type assertions #12942
Description
Activity
- changed the title
[-]Predicate types and 'this' type constraints[/-][+]Predicate type expressions and 'this' type constraints[/+]on Dec 15, 2016 - changed the title
[-]Predicate type expressions and 'this' type constraints[/-][+]Type predicate expressions and 'this' type constraints[/+]on Dec 15, 2016 I think some of my examples were a little bit misleading (edit: they have now been removed from the original comment) and I need to try to clarify what exactly is and is not in the scope of this idea (perhaps for myself as well):
This concept is about:
- Providing a way for
type,interfaceorclassdeclarations to provide a set of assertions on themselves, in the same sense that contracts assert over values. This is very similar in spirit toimplementsbut allows for more expressive grammar in the form of propositional logic statements and clauses over relations between types. In a rough sense, it allows interfaces, types, or classes to "implement" other interfaces, types, or class "shapes", or more complex mappings of them, and test for multiple combinations of these propositions, joined by logical connectives.
This concept is not about:
- Providing an alternative way to declare constraints on type parameters: I have considered what would be the implications of trying to achieve that using this powerful grammar and came to the conclusion that the level of potential complexity would not justify the value it gives.
I'll try to create a set of limiting rules to try to "reduce" the power (and also the potential traps) of these clauses.
One thing that can be done is introduce rules that limit what can participate in individual propositions. The first rule might be that every proposition must include the
thistype (or a name that corresponds the current type) at some form:These are ok:
interface MyInterface<T, U> where (this extends { [key: string]: [T, U] }) || ({ [key: string]: { a: T, b: U } } extends this) { // ... }
interface MyInterface<T, U> where keyof this extends "a" | "b" | "c" { // ... }
interface MyInterface<T, U> where this["a"] extends T[] && this["b"] extends { x: U } { // ... }
These are not:
interface MyInterface<T, U> where T extends Array<any> { // .... }
interface MyInterface<T, U> where Array<T> extends Array<number> { // .... }
interface MyInterface<T, U> where T extends Array<U> { // .... }
The reason I'm trying to disallow these patterns is because I'm considering what would happen if the type arguments given to
TandUare themselves polymorphic:interface AnotherInterface<A, B> where (...) { a: MyInterface<A, B>; }
If the
whereassertion was allowed to constrain its type parameters directly with this powerful grammar, it would mean that the compiler would need to "prove" that the type parametersAandBsatisfy a set of complicated constraints based on a whole different set of complicated constraints as premises, and that may be very difficult to do, or even eventually require the equivalent of a theorem prover.Instead, I'm trying to figure out a way to limit the power of these propositions to avoid trying to directly or indirectly enforce particular types over their parameters.
So the following
whereassertion should probably fail as well:interface MyInterface<T> where this extends { a: number } { a: T; }
Because it indirectly "forces" a type over an unknown polymorphic type.
But this would pass:
interface MyInterface<T extends number> where this extends { a: number } { a: T; }
I think the aim and direction here should become clearer now. I hope this helps..
- Providing a way for
- changed the title
[-]Type predicate expressions and 'this' type constraints[/-][+]Type predicate expressions and 'this' type assertions[/+]on Dec 16, 2016 Another way to express this is that the
whereassertion I'm describing here "validates" the final, resulting type, but does not participate in or influence its definition. It is not the same semantics aswherein C# , which is used for type parameter constraints.Maybe the keyword could be improved. Initially I considered
satisfies, which I thought didn't read very well, thenwhere. I'm consideringassertbut it could be a bit confusing as most people associate it with assertions over values, not types. I don't want to use keywords that may be useful for future features like code contracts (ensure,validate,expectetc.).Edit: How about
check?interface MyInterface<T, U> check (this extends { [key: string]: [T, U] }) || ({ [key: string]: { a: T, b: U } } extends this) { // ... }
type MyType<T, U> check SomePredicate<this, T, U> = { // ... }
I have intentionally generalized the ideas here as I felt doing that would be interesting and academically important. I wasn't necessary suggesting for it to be implemented the way it is presented here.
I'm closing this for now to prevent it from getting "tanked" forever by the TS team. Maybe I'll try to suggest something similar in the future in a more limited form.
- locked and limited conversation to collaborators
on Jun 19, 2018
The type system currently allows to specify basic type constraints (or 'assertions') on polymorphic types, for example:
However, currently there is no elegant syntax for types to declare constraints on themselves. Here's a real world example where I needed something like this:
I have a class containing several methods:
and I want to write a known (i.e. monomorphic) type that is constrained to exactly match the keys of
MyClass(the property types don't matter for this example, so I annotated them with{ ... }, for simplicity, also note thatMyTypeis intended to be user-customizable and cannot be deterministically generated using a mapped type):The best hack/workaround I found so far was to declare additional dummy types, that would fail if
MyTypedoesn't satisfy the expected constraint:The hack does seems to work so far, and has proven useful in practice, so by itself that's a significant improvement over having nothing at all. However, it is not very elegant or readable and violates type 'encapsulation'. It would be cool if I could instead write something like:
I'll try to break down what is happening here:
SameKeystype contains a type expression that evaluates to either thetrueorfalseboolean literal types. GivenP1,P2are valid type predicate expressions (e.g.T extends U) thenP1 && P2,P1 || P2!P1are also type predicate expressions etc.wherekeyword simply implies that the following type or type expression must evaluate to a boolean literal type and in particular totruefor the constrained type to be valid.[Edit: removed some confusing examples, see the next comment]
The concept can also be used with interfaces, or even classes:
Edit: changed the suggested
satisfieskeyword towhereas I believe it gives slightly better readability.