Typescript: Constraint Types Proposal

Created on 2 Jan 2017  ·  38Comments  ·  Source: microsoft/TypeScript

Edit: Now a gist, with all the previous edits erased.

See this gist for the current proposal. Better there than a wall of text here.

In Discussion Suggestion

Most helpful comment

Same here, mobx-state-tree would benefit highly from this feature

All 38 comments

Why not use a ternary operator?

Instead of:

let other: (value is number: string | value is string: boolean)

This:

let other: (value is number ? string : value is string ? boolean)

Looks less confusing for me.

this is what they call dependent types, isnt it?

if so, is predicate alone looks too constraining (pun intended) it should be any predicate:

type T<A> = string when A is number && A % 2 === 0;
type T<A> = number when A is string && A.length > 1;

@aleksey-bykov It's not quite dependent types, because values cannot be lifted to types, nor can types be lifted to values. This proposal only allows proper type -> type functions (we already have value -> value functions), but it's missing the other two cases. And no, typeof does not provide that ability, because it merely grabs the type of that binding, and is equivalent to using a type alias with explicit types.

a is T already exists for things like this:

interface ArrayConstructor {
    isArray(value: any): value is any[];
}

And Is<Type, Super> is a static instanceof assertion, and you can use it like this:

function staticAssert<T>() {}
declare const value: any

if (Array.isArray(value)) {
    staticAssert<Is<typeof value, any[]>>()
} else {
    staticAssert<NotA<typeof value, any[]>>()
}

@asfernandes

The reason I chose not to use a ternary is because of two reasons:

  1. It visually conflicts with nullable types, and is also much harder to parse (you have to parse the whole expression before you know if the right side is a nullable type):

    type Foo = (value is number ? string : boolean)
    type Foo = (value is number? ? string : boolean)
    
  2. Types may have more than two different conditions (e.g. with overloaded types), and the ternary operator doesn't make for very readable code in this case.

i suggest you list type overloads one per line (rather than all in the samr
| separated list), it's cleaner, proven to work, doesn't need 3nary
operator

On Jan 8, 2017 8:03 PM, "Isiah Meadows" notifications@github.com wrote:

@asfernandes https://github.com/asfernandes

The reason I chose not to use a ternary is because of two reasons:

1.

It visually conflicts with nullable types, and is also much harder to
parse (you have to parse the whole expression before you know if the right
side is a nullable type):

type Foo = (value is number ? string : boolean)type Foo = (value is number? ? string : boolean)

2.

Types may have more than two different conditions (e.g. with
overloaded types), and the ternary operator doesn't make for very readable
code in this case.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/13257#issuecomment-271195552,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AA5PzZI0WDoZcUpIVQ6w65Z8dCdTSOecks5rQYdlgaJpZM4LZObU
.

@aleksey-bykov TypeScript only cares about newlines for ASI, and there's no other precedent for newline significance. Additionally, I intentionally phrased the constraint types as a union of constraints, so they could be similarly narrowed with control flow (just as current union types can be), and that's why I used the | union operator.

As for why I chose a union instead of separate type overloads, here's why:

  1. I don't need to create a dedicated impossibility type, and can just declare any particular token as a keyword. This limits the compatibility impact and simplifies parsing greatly.

  2. Type aliases will remain to always resolve to something, so it doesn't involve rewriting core logic that relies on this assumption. And the TypeScript code base has historically been a giant ball of mud with assumptions like these littered across the code base.

  3. I don't have to create a type alias to define such a type every time. This not only provides flexibility (I can define one any time I want), but it also lets me create lambda return types that depend on the types of their parameters, even if they're unions or generics whose type is detected at runtime. The latter is plainly not possible with named types.

  4. Return types can statically depend on the generic type parameter as well as the parameters themselves. This is a must for capturing and properly constraining Promise.resolve as a standalone method, since it's literally impossible to ever return a Promise<Thenable<T>> due to thenable coercion.

I did in fact already consider the idea of type overloads, and initially tried that route before posting this. The above issues, especially the first three, are why I elected to go a different route instead.

@isiahmeadows, your proposal is what I would like to see in TypeScript.
However, I do not think that this is the union. It is the ordered selection, not all alternatives together (like in PEG). And narrowing should be designed from the scratch for this kind of types.

I would like to use a / instead of | to emphasize that the order matters (the same intuition as PEG)

However, I wanted to move in the direction of type decomposition via switch-types

type CoerceToValue<T> = switch(T)  { 
     case<V> Thenable<V>: V;
     default: T;
}  

or even

type CoerceToValue<T> = switch(T)  { 
     case<V> Thenable<V>: CoerceToValue<V>;
     default: T;
}  

@Artazor That might work, too, but I have my reservations in terms of expressiveness. How would you do something equivalent to my Super<T> type (any supertype of T), and how would you constrain the T correctly with Promise<T> (where T cannot be a thenable)?

// Using my proposal's syntax
type Super<T> = (U in T extends U: U);
interface Promise<T extends (T extends Thenable<any>: * | T: T)> {}

What about never as impossibility assertion?

@bobappleyard Can't use it because it's already a valid return type (specifically a bottom type, a subtype of every type).

function error(): never {
  throw new Error()
}

I need an impossibility type, where even merely matching it is contradictory.

Note to all: I updated my proposal some to aid in readability.

I'd like to solve something like https://github.com/Microsoft/TypeScript/issues/12424.

I just want to check, if I have understood this proposal correctly.

Say I have this example:

// some nested data

interface Data {
  foo: string;
  bar: {
    baz: string;
  };
}

const data: Data = {
  foo: 'abc',
  bar: {
    baz: 'edf'
  }
};

// a generic box to wrap data

type Box<T> = {
  value: T;
};

interface BoxedData {
  foo: Box<string>;
  bar: Box<{
    baz: Box<string>;
  }>;
}

const boxedData: BoxedData = {
  foo: {
    value: 'abc'
  },
  bar: {
    value: {
      baz: {
        value: 'edf'
      }
    }
  }
};

I'd like to express BoxedData in a generic way. Would this be correct?

// should box everything and if T is an object,
//   - it should box its properties
//   - as well as itself in a box
//   - and arbitrary deep
type Boxing<T> = [
    T is object: [P in keyof T]: Boxing<T[P]> & Box<T>,
    T: Box<T>,
]

type BoxedData = Boxing<Data>

And how would it look like with arrays? E.g.

interface Data {
  foo: string;
  bar: {
    baz: string;
  };
  foos: Array<{ foo: string }>;
}

// arrays should be boxed, too and all of the items inside of the array
type Boxing<T> = [
    T is object: [P in keyof T]: Boxing<T[P]> & Box<T>,
    T is Array<ItemT>: Boxing<Array<ItemT>> & Box<T>,
    T: Box<T>,
]

type BoxedData = Boxing<Data>

const boxedData: BoxedData = {
  foo: {
    value: 'abc'
  },
  bar: {
    value: {
      baz: {
        value: 'edf'
      }
    }
  },
  foos: {
    value: [
      {
        foo: { value: '...' }
      }
    ]
  }
};

My use case are forms which can take any data (nested and with arrays) as an input and have a data structure which matches the input data structure, but with additional fields (like isValid, isDirty and so on).

@donaldpipowitch Your Boxing<T> type is almost correct.

// arrays should be boxed, too and all of the items inside of the array
type Boxing<T> = [
    T extends object: {[P in keyof T]: Boxing<T[P]> & Box<T>},
    U in T extends U[]: Boxing<U[]> & Box<T>,
    T: Box<T>,
]

Note a few changes:

  1. variable is Type requires the LHS to be a value binding (e.g. let foo = ...), not a type binding (e.g. type Foo = ...). What you're really looking for is T extends Type.
  2. I fixed your mapped type to use the correct syntax.
  3. I used an existential on the array line to capture the array item type.

@donaldpipowitch Updated with my newest changes:

type Boxing<T> = [
    case T extends object: {[P in keyof T]: Boxing<T[P]> & Box<T>},
    case U in T extends U[]: Boxing<U[]> & Box<T>,
    default: Box<T>,
]

I added keywords to make it a little more readable and obvious at the cost of a little extra verbosity.

Your first set of examples are still using the old syntax

On 27 Mar 2017 16:36, "Isiah Meadows" notifications@github.com wrote:

@donaldpipowitch https://github.com/donaldpipowitch Updated with my
newest changes:

type Boxing = [
case T extends object: {[P in keyof T]: Boxing},
case U in T extends U[]: Boxing,
default: Box,
]

I added keywords to make it a little more readable and obvious at the cost
of a little extra verbosity.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/13257#issuecomment-289491739,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AACMHAYXqhpJSdWGZnoiPmGHk2z7xIVGks5rp9d7gaJpZM4LZObU
.

@bobappleyard Thanks for the catch! Should be fixed now.

@isiahmeadows Thank you. This looks really useful for our use case. I hope this will be implemented someday ❤

that little extra verbosity finally makes it readable to a layman, such as myself. :P

@MeirionHughes Welcome

Looks like a great idea with a lot of immediate uses :) Any idea when this might hit production? :)

Same here, mobx-state-tree would benefit highly from this feature

@isiahmeadows

I want to write types for Telegram API Update type. Are this right types?

type XMessage = { message: Message }
type XEditedMessage = { edited_message: Message }
type XChannelPost = { channel_post: Message }
type XEditedChannelPost = { edited_channel_post: Message }
type XInlineQuery = { inline_query: InlineQuery }
type XChosenInlineResult = { chosen_inline_result: ChosenInlineResult }
type XCallbackQuery = { callback_query: CallbackQuery }

type Update<X> = [
   case X is XMessage: X,
   case X is XEditedMessage: X,
   case X is XChannelPost: X,
   case X is XEditedChannelPost: X,
   case X is XInlineQuery: X,
   case X is XChosenInlineResult: X,
   case X is XCallbackQuery: X,
   default: throw
]

const value = getUpdate()
const u: Update<typeof value> = value

@goodmind Constraint types do not resolve this. (See edit) You would be interested in this proposal with disjoint types.

The expression value is Type takes a variable name on the left hand side, like in let value = 2, not a type name like in interface X {}. It's an expanded version of the existing return-only type, which similarly only accepts a parameter name, like in isArray(value: any): value is any[] within the standard library.

Edit: It is theoretically possible as demonstrated in this code snippet, but given union/intersection types, you might prefer a dedicated operator anyways.

type Xor<A if constraint A, B if constraint B> = [
    case A extends true: B extends false,
    default: B extends true,
];
type Disjoint<A, B> = [default U in: Xor<U extends A, U extends B>];

@isiahmeadows what is if constraint A ?

@goodmind I updated the proposal with some new types and cleaned it up quite a bit when I moved it to a gist.

Thank you for the effort. Do you know what would be the next steps to get something like this implemented?

@isiahmeadows it is sometimes cumbersome to read this gist (and Xor and Disjoint types as well). Can you add more code examples to gist?

@goodmind I'll update it with the constraint types explained better. I'll also introduce a high-level summary, to make it easier to understand.

@goodmind Updated the gist. Also, the Disjoint type was wrong (it returned the Xor result rather than a mapped case variable), so I corrected it. As for Xor, I just used the common "xor" abbreviation for the logical "exclusive or" (either, but not both).

Gist updated with compile-time assertions

Would this proposal allow someone to write an type/interface that represents just the readonly properties of a type?

Like

type WritableKeys<T> = // somehow?
type ReadonlyKeys<T> = // somehow?
type WritablePart<T> = { [K in WritableKeys<T>]: T[K] };
type ReadonlyPart<T> = { [K in ReadonlyKeys<T>]: T[K] };
type Identityish<T> = WritablePart<T> & Readonly<ReadonlyPart<T>>;

so that you can write a safe version of

function unsafeSet<T,K extends keyof T>(obj: T, prop: T, val: T[K]) {
    obj[prop] = val; // what if T[K] is readonly?  
}

like

function safeSet<T,K extends WritableKeys<T>>(obj: T, prop: T, val: T[K]) {
    obj[prop] = val;
}

Similarly for making an interface from a class, excluding any private members:

type PublicKeys<T> = // somehow?
type ProtectedKeys<T> = // somehow?
type PrivateKeys<T> = // somehow?
type PublicPart<T> = { [K in PublicKeys<T>]: T[K] };  

These are constraints on types but I'm not sure if there's any way to express them. If this is not the appropriate issue I'll find or create another one. Thanks!

@jcalz No, because type-level property reflection is out of scope of this proposal.

Thanks.

EDIT: wait, couldn't you do something like

type ReadonlyPart<T> = {
    [K in keyof T]: [case ({K: T[K]} is {readonly K: T[K]}): T[K]];
}

?

@jcalz That doesn't work (for readonly), for two reasons:

  1. The LHS of value is Type is restricted to identifiers (for now), to ease general implementation.
  2. Read/write properties aren't soundly checked ATM (bug), and the correct behavior would have read-write subtype readonly (i.e. RW extends RO), but not vice versa.

Assuming the latter and #15534 are both fixed, you would need to do this instead:

type ReadonlyKeys<T> = keyof ReadonlyPart<T>;
type ReadonlyPart<T> = {
    // Check if not assignable to read/write.
    [K in keyof T]: [case !(T extends {[K]: T[K]}): T[K]];
};

For any other modifier (public, private, protected), it still remains impossible, because TypeScript does not expose those at all at the type level outside their scope.


I thought there was likely a way with readonly properties, but couldn't come up with one initially.

Are there any active plans to implement this proposal in TypeScript?

@ajbouh check conditional types PR: https://github.com/Microsoft/TypeScript/pull/21316

@ajbouh Item of note: it's really only implementing part of it. Specifically, a variant of my proposal is being implemented right now:

  • Conditional type (like my [case ...]): #21316
  • Existential type (like my [case U in ...]): #21496 (note: like mine, it's restricted to the condition part of conditional types)
  • Constraints as types: no issue I'm aware of*
  • Impossible type (my throw): no issue I'm aware of*

The third point could be easily shimmed with some boilerplate in terms of the first, but the fourth currently cannot.

As for a quick TL;DR, for those who aren't fully aware of what each one is, or how it's implemented:

  • Conditional type: this is like a lazy if/else, but for types. The proposal being implemented is literally a type-level ternary operator, the same conceptually as the value-oriented version you already know.
  • Existential type: this is your generic "for every item", but instead of you having to use any, you can still be safe about it when you pass it to another whatever, and it just works. They use infer at each use site within the condition, where I specify it outside the type.
  • Constraints as types: this is exactly how it sounds - T extends Whatever being equivalent to the in-progress T extends Whatever ? true : false, and the ternary just being SomeConditionalBoolean ? Foo : Bar. It hasn't been outright rejected, but the TS team has noted it would be rather difficult.
  • Impossible type: this is exactly what you'd expect: a type that's an error if ever evaluated. I have not received any indication, nor found/read any, from the TS team that they are for or against this, but it's a pretty natural continuation of having conditional types - you might want to conditionally reject a particular type. (Promises come to mind here - a Promise<Promise<T>> type is literally impossible.)

* TS team: if there is an issue open for either of these two, please tell me so I can edit this in.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blendsdk picture blendsdk  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

zhuravlikjb picture zhuravlikjb  ·  3Comments

manekinekko picture manekinekko  ·  3Comments