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

Pattern Matching Support #165

Closed
dignifiedquire opened this issue Jul 21, 2014 · 64 comments
Closed

Pattern Matching Support #165

dignifiedquire opened this issue Jul 21, 2014 · 64 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@dignifiedquire
Copy link

I've seen some short notions about pattern matching on code plex, but no serious discussions around it. As I'm a big fan of it, I've written up some small examples on how I think pattern matching could look in TypeScript.

This is by far no complete specification, just some ideas that I want to bring forward and have a little discussion around.

Function Overloading

// Pattern matching on functions

function pickCard {
  (x: {suit: string; card: number; }[]): number {

    var pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;  

  }
  (x: number): {suit: string; card: number; } {

    var pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };

  }
} 
// Translation of functions.ts using sparkler
// https://github.com/natefaubion/sparkler

function pickCard(x) {
  [...{ suit @ String, card @ number }] =>  {
    var pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;  
  }
  x @ Number => {
    var pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
} 

Match statement

// Pattern matching using match

function whoami (x: any): string {

  match x {
    case { name: string, type: 'person' }: 
        return 'Person: ' + name;

    case { name: string, type: 'animal' }: 
        return 'Animal: ' + name;

    default: 
        return 'Unkown you';
  }
} 
// Pattern matching using match translated

function whoami (x) {

  match x {
    case { name @ String, type: 'person' }: 
        return 'Person: ' + name;

    case { name @ String, type: 'animal' }: 
        return 'Animal: ' + name;

    default: 
        return 'Unkown you';
  }

} 
@RyanCavanaugh
Copy link
Member

We've typically avoided features that require type information at runtime, because many types in TypeScript are indistinguishable from a runtime perspective.

For example, what would the emitted JavaScript look like in this case?

enum Suit { Spades, Hearts, Clubs, Diamonds }
enum Rank { Ace, King, Queen, Jack }
function pickCard {
    (s: Suit) : number { return 1; }
    (r: Rank): number { return 0; }
}

pickCard(Suit.Spades);
pickCard(undefined);

@justinmchase
Copy link

@RyanCavanaugh You could potentially build in an Ometa-like library that allowed you to do pattern matching of all sorts. You may want a more TS friendly syntax but it would be very compelling. You could do matching on any object not just Types. Here's an example
http://www.tinlizzie.org/ometa-js/#Typechecker

@AlexGalays
Copy link

"We've typically avoided features that require type information at runtime"

Will that change with the accommodating of the angular2 reflection?

@mhegazy
Copy link
Contributor

mhegazy commented Mar 10, 2015

@AlexGalays, i do not think so. one thing we are trying to maintain is keep a single module transpilation possible with no semantic information present.

@refi64
Copy link

refi64 commented Aug 24, 2015

I have to say, something like this would be insanely nice. What are the chances of this happening?

@SamuelMarks
Copy link

👍

3 similar comments
@dev-tim
Copy link

dev-tim commented Oct 5, 2015

👍

@svanderbleek
Copy link

👍

@slavah
Copy link

slavah commented Oct 23, 2015

👍

@tejacques
Copy link

Just wanted to point out a possible alternative that may be available with --noImplicitReturns with the modification mentioned here #5916.

This gets you code that looks like this, and requires no changes to TypeScript whatsoever
interface SomeType {
    stringValue: string
}

function isSomeType(x: any): x is SomeType {
    return x && typeof(x.stringValue) === 'string';
}

function foo(x: number | string | boolean | SomeType): string {
    if(typeof x === 'number' || typeof x === 'boolean') {
        return x.toString();
    } else if (typeof x === 'string') {
        return x;
    } else if (isSomeType(x)) {
        return x.stringValue;
    } else {
        return 'other';
    }
}

--noImplicitReturns gives us full spectrum matching in a very conventionally JavaScript way. We can now make a pretty trivial helper function match:

function match<T, U>(value: T, matcher: (t: T) => U) {
    return matcher(value);
}

And we can use it like this:

const res = match(1, (x: string | boolean | number | SomeType): string => {
    if(typeof x === 'number' || typeof x === 'boolean') {
        return x.toString();
    } else if (typeof x === 'string') {
        return x;
    } else if (isSomeType(x)) {
        return x.stringValue;
    } else {
        return 'other';
    }
});

and

const value: string | boolean | number | SomeType = 5;
const res = match(value, x => {
    if(typeof x === 'number' || typeof x === 'boolean') {
        return x.toString();
    } else if (typeof x === 'string') {
        return x;
    } else if (isSomeType(x)) {
        return x.stringValue;
    } else {
        return 'other';
    }
});

With full safety that we have handled all cases in the union.

New proposal for syntactic sugar

Make match a built-in construct which would look like this:

A language construct that takes an object mapping the type (property name) to a function or literal.

const res = match (value) {
    string: s => s,
    boolean: b => b.toString,
    number: n => n.toString(),
    SomeType({ stringValue }) { return stringValue },
    default: 'other'
}

All that's happening here is it creates a function for you that creates an if(isType(value)) return typeFn(value) statement, for each function passed in. For primitive types, it uses: typeof (value) === '<primitive>', for "tagged" or disjoint unions specified with type literals, the field containing the literal type could be checked, and for any other type, it checks to see if a function is<Type> exists in scope.

So this maps out to the following code:

const res = ((x: string | boolean | number | SomeType): string => {
    if (typeof x === 'string') {
        return ((s: string) => s)(x);
    } else if (typeof x === 'boolean') {
        return ((b: boolean) => b.toString())(x);
    } else if(typeof x === 'number') {
        return ((n: number) => n.toString())(x);    
    } else if (isSomeType(x)) {
        return (({ stringValue }: SomeType) => stringValue)(x);
    } else {
        return 'other';
    }
})(value);

This somewhat addresses @RyanCavanaugh's concern about enum types or other things which have their type information erased at runtime in the sense that because no isSuit or isRank type guard exists in scope, it would give a compiler error. It could also conceivably give a compiler error in the case of overlapping type literals, since all their values are known at compile time.

As an optimization, if the type being matched on is purely composed of a "tagged"/disjoint union based on type literals, it could output the following:

type Circle = {
    shape: 'square'
    radius: number
}
type Rectangle = {
    shape: 'rectangle'
    length: number
    width: number
}
type Square = {
    shape: 'square'
    length: number
}

// because these have a shared literal property
// that will be used as the implicit "tag", and TS
// can know this is a tagged union
type Shape = Circle | Rectangle | Square

const shape: Shape = getShape(); // some shape
function getArea(shape: Shape) {
    return match shape {
        Circle: ({ radius }) => 3.14 * (radius * radius),
        Rectangle: ({ length, width }) => length * width,
        Square: ({ length }) => length * length,
    };
}

The getArea function would be translated in an optimized way to look like this:

function getArea(shape: Shape) {
    return ({
        /* Circle.shape literal    */ 'circle': ({ radius }) => 3.14 * (radius * radius),
        /* Rectangle.shape literal */ 'rectangle': ({ length, width }) => length * width,
        /* Square.shape literal    */ 'square': ({ length }) => length * length,
    })[shape.shape](shape);
}

@ghost
Copy link

ghost commented Feb 11, 2016

👍

But why not erlang style function guards? The syntax proposed above is a little funky.

function animal(species: Cat) {
    species.meow();
}

function animal(species: Dog) {
    species.bark();
}

becomes:

function animal(species: Cat | Dog) {
    if (species instanceof Cat) {
        species.meow();
    } else if (species instanceof Dog) {
        species.bark();
    }
}

or checking values:

function animal(alive: true) : string {
    return "Is alive";
}

function animal(alive: false) : string {
    return "Is not alive";
}

@felixSchl
Copy link

Personally, I wouldn't call it funky, since it's pretty common (i.e. found a lot of adoption). Here's a incomplete list of languages that come to mind:

There are ought to be plenty more, those just came to the top of my head.

You can check values using guards then, i.e. case x:xs | x == 0 to check if this is a list where the first element is a 0.

Your suggestions reminds me of multi-methods in lisp or function lookup based on arity like in elixir.

@tejacques
Copy link

@Zvxy it sounds more like your asking for a feature to automatically combine function overloads into a single function. It's a nice idea, but it wouldn't work for interfaces unless TypeScript could automatically create an interface type guard, which is impossible in ambiguous cases.

@ghost
Copy link

ghost commented Feb 11, 2016

@felixSchl

I couldn't really find a better word for it than "funky", but you're right, this isn't pattern matching and is actually arity overloading. I do code in rust occasionally (and elixir/haskell is love). Here's a quick way I had to do it in rust:

trait Multiply {
    fn multiply(self, i32) -> i32;
}

impl Multiply for String {
    fn multiply(self, n: i32) -> i32 {
        self.parse::<i32>().unwrap() * n
    }
}

impl Multiply for i32 {
    fn multiply(self, n: i32) -> i32 {
        self * n
    }
}

fn test<T: Multiply>(x: T, n: i32) -> i32 {
    x.multiply(5)
}

fn main() {
    println!("{}", test("5".to_owned(), 5)); //25
    println!("{}", test(5, 5)); // 25
}

Rust is notoriously verbose. I'd like to just do this:

fn test(x: String, n: i32) -> i32 {
    self.parse::<i32>().unwrap() * n
}

fn test(x: i32, n: i32) -> i32 {
    x * n
}

But I can't...

@tejacques

You're right, I had forgotten about interfaces (I have been switching between languages). Do you have any alternative solutions?

@felixSchl
Copy link

@Zvxy As @tejacques correctly pointed out we are talking about different things. But still, here is what this could look like in rust, using ADTs and matching:

enum Foo {
    Foo(String),
    Bar(i32),
}

fn test(x: Foo, n: i32) -> i32 {
    n * match x {
        Foo::Foo(s) => s.parse::<i32>().unwrap(),
        Foo::Bar(n) => n,
    }
}

#[test]
fn it_works() {
    assert_eq!(test(Foo::Foo("5".to_owned()), 5), 25);
    assert_eq!(test(Foo::Bar(5), 5), 25);
}

Not that verbose. I see where you are coming from, though. I missed function overloading by type for quite a while when leaving C#, but once I got the hang of ADTs, I don't really see the point anymore.

@ghost
Copy link

ghost commented Feb 11, 2016

@felixSchl yeah, I had edited my post right before you responded about it being arity overloading and not pattern matching, my mistake.

You said you don't really see the point anymore, but as far as I'm aware that code is going to be checked during runtime, not compile time. This is going a bit off topic though.

@alrz
Copy link

alrz commented Feb 11, 2016

why not extending switch statement, just like C#.

@felixSchl
Copy link

@alrz The answer is in your question - because it's a statement. See how in the rust example above I could multiply the result of the match expression? Statements are inflexible and enforce imperative style coding. Take try/catch, for example, or the for loop, while loop, any loop. Personally, I wish languages would part entirely from these constructs. Let's even take if as an example. Here's some contrieved purescript:

let y = if x == 0 then "foo" else "bar"

which you would need to do imperatively using if statements:

let y: string; // <- I have to tell the compiler the type and cannot at compile time reason about if this value is going to null or not :(
if (x == 0) {
    y = "foo";
} else {
    y = "bar";
}

To be fair, there are ternary expressions in js/ts:

const y = (x == 0) ? "foo" : "bar";

@tejacques
Copy link

@Zvxy I think the only thing you can do for interfaces (at least that I can think of) is check the scope for a function that guards on that interface, but this is far from perfect.

@gneuvill
Copy link

👍

@baio
Copy link

baio commented May 29, 2016

You could do redundant pattern matching right now

class Some {
    constructor(public val: string) {       
    }
}

class None {
}


type Maybe = Some | None;

var a = new Some("val");

if (a instanceof Some) {
    console.log(a.val);
}

But some syntax sugar would be appreciated.

@ablankholm
Copy link

Any reason https://github.com/bendetat/SimplicityJS wouldn't work with TS?

@schickling
Copy link

Pattern matching is one of the most useful features of "more powerful" languages like Swift, Scala and Rust. I'd highly welcome this feature as part of Typescript!

@mmc41
Copy link

mmc41 commented Sep 28, 2016

@tejacques Do you have an update of your interesting example for typescript 2.0 ?

@aesteve
Copy link

aesteve commented Oct 18, 2016

This is definitely a feature that would make me switch to TypeScript for every project.

Whenever you're dealing with a CQRS architecture, pattern matching has to come along the way.

That'd allow people to create a strongly typed Flux / Redux implementation. For now, only Elm offers such possibility.

Strongly-typed actions + type aliases + ADT + pattern matching are a bless when dealing with such architecture. You're basically covered that if the code compiles, it's gonna work pretty much without runtime exceptions.

If you declare a reducer for a certain type of action. Your whole chain is now strongly typed.

In current implementations :

function updateState(action, state) {
  switch(action.type) { // a String...
    case 'create':
      var newTodo = action.data; // is data really an object ? shouldn't the compiler know ?
      state[newTodo.id] = newTodo; // and if state is not purely a map ? what if todo as no 'id' ?
  }
}

With type algebra + pattern matching this would become (sorry for pseudo-code) :

type alias Todo = {id: String, items: Array[String]}
val CreateTodo = Action(Todo)
val RemoveTodo = Action({id: String})
type alias TodoAction = CreateTodo | RemoveTodo
type alias SomeState = Map[Todo]
function updateState(action: TodoAction, state: SomeState) {
  match action.type {
    case CreateTodo newTodo => // here the compiler knows that newTodo is {id: String, items: Array[String]})
    case UpdateTodo {id} => state.remove(id)
    // => no default, the compiler wouldn't compile if I forgot about some case
  }

} 

Typescript seems to already offer the strongly-typed system, the destructuring ability. The only thing this example misses would be pattern matching to be a little more readable, and more safety, and let the compiler check for most of the errors that today occur in production (example : payload.action doesn't have the right type)

@tekacs
Copy link

tekacs commented Oct 18, 2016

@aesteve In today's TypeScript 2.0 code, your 'pseudocode' can be written as follows and works fine:

Preamble:

interface Todo {
  id: string
  items: string[]
}

interface CreateTodo {
  kind: 'create'
  todo: Todo
}

interface RemoveTodo {
  kind: 'remove'
  id: string
}

type TodoAction = CreateTodo | RemoveTodo;

type SomeState = Map<string, Todo>;

function updateState(action: TodoAction, state: SomeState) {
  switch (action.kind) {
    case 'create': // here the compiler knows that action is CreateTodo, of {id: String, items: string[]}
    case 'remove': state.delete(action.id)
    default: const _exhaustiveCheck: never = action
  }
}

The argument about automatic exhaustivity checking for the above seems to have been had in #9260 (see the last few comments including my own for a summary).

I've started to propose (but haven't had time yet to formalise) doing this with overloading in #10765, so that it would look like this (for the times that it's more convenient to write these separately):

function updateState(action: CreateTodo, state: SomeState) {
  // here the compiler knows that the action is CreateTodo
}

function updateState(action: DeleteTodo, state: SomeState) {
  state.delete(action.id)
}

function updateState(action: TodoAction, state: SomeState): never {} // unclear if there's a better syntax for doing this bit

@xogeny
Copy link

xogeny commented Nov 9, 2016

Much of what is being asked for in pattern matching is already possible in TS 2.0.0 (Thanks!). It is achieved without storing any runtime type information too. But for me, the one thing that would be nice would be more intuitive syntax. For example, right now I can write code like this.

class Foo {
  public name: string;
}

class Bar {
  public title: string;
}

type Stringable = A | B;

function stringOf(a: Stringable): string {
  if (a instanceof Foo) return a.name;
  return a.title;
}

That's great. But something like this would just be easier to read:

function stringOf(a: Stringable): string {
  a match {
     case Foo: return a.name;
     case Bar: return a.title;
 }
}

My point here is that using this strictly with classes could avoid some of the "having to include runtime information" because you could piggy back off of what is already included (i.e., the same information that instanceof is using). Of course, instrumenting this with destructuring and guards would be really cool too! e.g.,

function lengthOf(a: Stringable): number {
  a match {
    case Foo({ name }) if name: return name.length;
    case Bar({ title }) if title: return title.length;
    default: return 0;
  }
}

My guess is that what's already been implemented with type narrowing could be completely reused here. As far as I can tell, this is just syntactic sugar for:

function lengthOf(a: Stringable): number {
  if (a instanceof Foo && a.name) return a.name.length;
  else if (a instanceof Bar && b.title) return a.title.length;
  else return 0;
}

...with the same possibilities for detection mutual exhaustion.

Or am I missing something?

@gbegher
Copy link

gbegher commented Oct 5, 2017

Here's another implementation of pattern matching for sum types using dynamic dispatch:

// Just for readability
type Sum<Components> = Components

// Keys are the names/indices of the components, values are the types of the components
type SomeSumType = Sum<{
    NUMBER: number
    STRING: string
}>

// ST is for SumType
interface CaseOf<ST> {
    match: <T>(pattern: Pattern<ST, T>) => T
}

// T is for TargetType
type Pattern<ST, T> = {
    [key in keyof ST]: (cas: ST[key]) => T
}

const createCase = <ST, K extends keyof ST>(k: K, value: ST[K]): CaseOf<ST> => ({
    match: <T>(pattern: Pattern<ST, T>) => pattern[k](value)
})

const a: CaseOf<SomeSumType> = createCase('NUMBER', 5)
const b: CaseOf<SomeSumType> = createCase('STRING', 'someString')
const notA: CaseOf<SomeSumType> = createCase('NUMBER', 'wrongType')  // Type error
const wrongCase: CaseOf<SomeSumType> = createCase('C', 'test')  // Type error

const match = <CS, T>(cas: CaseOf<CS>, pattern: Pattern<CS, T>) => cas.match(pattern)

const result = match<SomeSumType, boolean>(a, {
    NUMBER: num => num > 6,
    STRING: str => str === 'expectedString'
})

@laser
Copy link

laser commented Nov 6, 2017

Achieving a similar effect with Church encoding, as per Rúnar Bjarnason:

interface Shape {
    match<T>(a: (x: Square) => T, b: (y: Rectangle) => T, c: (z: Circle) => T): T;
}

class Square implements Shape {
    constructor(public size: number) { }
    match<T>(a, b, c): T { return a(this); }
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) { };
    match<T>(a, b, c): T { return b(this); }
}

class Circle implements Shape {
    constructor(public radius: number) { };
    match<T>(a, b, c): T { return c(this); }
}

let x: Shape = new Square(10);

console.log(x.match(
    (sqr) => { return sqr.size * sqr.size },
    (rec) => { return rec.width * rec.height },
    (cir) => { return Math.PI * cir.radius ** 2 }));

The major problem with this approach (in TypeScript) is that I see no way to prevent library consumers from subclassing Shape.

@xogeny
Copy link

xogeny commented Nov 6, 2017

I've used a similar pattern. I'm not sure the problem is with subclassing (implementing, really) Shape. After all, they will have to provide an implementation of match, so that is on them.

To me, the bigger issue is when you have lots of mutually exclusive possibilities. It would end up being a bit awkward. But it works and, as I said, I have used this approach.

@xogeny
Copy link

xogeny commented Nov 6, 2017

Ah...I think I see what you mean by extending Shape. I suppose you are referring to the problem of what to do if they want to add another possibility to the list of possibilities (i.e., growing it from 3 to 4 to ...). In Scala, this is solved by sealing the class (thus freezing the possible subtypes). We don't have that in TypeScript of course.

If you have an open ended set of possibilities, you are probably better of simply defining a perimeter method. If it is a closed set of possibilities (and you don't have the option to "seal" them as in Scala), I think the approach you showed works just fine because the match method effectively closes the set of possibilities for you.

If you really wanted to, I suppose you could do something like this:

interface ShapeHandler<T> {
    square: (x: Square) => T;
    rectangle: (y: Rectangle) => T;
    circle: (z: Circle) => T;
}
interface Shape {
    match<T>(handler: ShapeHandler<T>): T;
}

class Square implements Shape {
    constructor(public size: number) { }
    match<T>(handler: ShapeHandler<T>): T { return handler.square(this); }
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) { };
    match<T>(handler: ShapeHandler<T>): T { return handler.rectangle(this); }
}

class Circle implements Shape {
    constructor(public radius: number) { };
    match<T>(handler: ShapeHandler<T>): T { return handler.circle(this); }
}

let x: Shape = new Square(10);

console.log(x.match({
    square: (sqr) => { return sqr.size * sqr.size },
    rectangle: (rec) => { return rec.width * rec.height },
    circle: (cir) => { return Math.PI * cir.radius ** 2 }
}));

interface ExtendedShapeHandler<T> extends ShapeHandler<T> {
    triangle: (t: Triangle) => T;
}

class Triangle implements Shape {
    constructor(public a: number, public b: number, public c: number) { };
    match<T>(handler: ExtendedShapeHandler<T>) { return handler.triangle(this) }
}

console.log(x.match({
    square: (sqr) => { return sqr.size * sqr.size },
    rectangle: (rec) => { return rec.width * rec.height },
    circle: (cir) => { return Math.PI * cir.radius ** 2 },
    triangle: (tri: Triangle) => { return tri.a + tri.b + tri.c },
} as ShapeHandler<number>));

I kind of like using named handlers (although it seems as though the type checker would always ensure you didn't have them mixed up). That cast is necessary to keep TypeScript from complaining. But it is still type safe since it is just loosening the semantics around object literals. It still ensures that the value actually has the structure of a ShapeHandler and just seems the triangle bit as "extra" (although the Triangle won't see it that way). But this is still unsafe because we have no way to ensure that an extended shape will get passed an ExtendedShapeHandler.

@AlexGalays
Copy link

That's still quite a lot of boilerplate compared to modern FP languages, plus we don't all use classes :p (I avoid them like the plague)

@goodmind
Copy link

goodmind commented Nov 6, 2017

How would you do proper pattern matching in TypeScript if types are erased at runtime? Isn't this pointless

@AlexGalays
Copy link

AlexGalays commented Nov 6, 2017

@goodmind JVM languages such as scala also have to deal with this issue because of type erasure. It's still a very useful tool to have nonetheless and read better than imperative if/else statements.

See https://github.com/tc39/proposal-pattern-matching where everything can be checked at runtime. Although, Things like { x, ... y }: /* match an object with x, stuff any remaining properties in y */ would have to raise compilation errors if the compiler can't know for a fact what the type for y is, when in strict mode.

@goodmind
Copy link

goodmind commented Nov 7, 2017

@AlexGalays It's stage 0 tho

@xogeny
Copy link

xogeny commented Nov 7, 2017

@AlexGalays As far as boilerplate goes, I totally agree. I love pattern matching in Rust, Scala, etc. So thanks for pointing out that proposal. That would be very cool! I'll have to keep an eye on that.

@laser
Copy link

laser commented Nov 7, 2017

A discriminated union-based approach may also be an option for you. As per the docs:

type aliases cannot be extended or implemented from (nor can they extend/implement other types)

...which addresses the issue I mentioned earlier re: preventing programmers from adding new implementations of Shape.

Note that exhaustiveness in the switch/case used in the match method is guaranteed by the --strictNullChecks compiler flag plus a return type of T.

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function shape<T>(s: Shape, a: (x: Square) => T, b: (y: Rectangle) => T, c: (z: Circle) => T): T {
    switch (s.kind) {
        case "square": return a(s);
        case "rectangle": return b(s);
        case "circle": return c(s);
    }
}

let x: Square = { "kind": "square", "size": 10 };

console.log(shape(x,
    (sqr) => { return sqr.size * sqr.size },
    (rec) => { return rec.width * rec.height },
    (cir) => { return Math.PI * cir.radius ** 2 }));

@masaeedu
Copy link
Contributor

masaeedu commented Nov 7, 2017

@laser You can already do very general discriminator-based pattern matching in JS by doing something like:

const match =  (p, ...c) => {
  const cases = new Map(c)
  return x => cases.get(p(x))(x)
}

const area = match(
  x => x.kind,

  ["square", ({ size }) => size ** 2],
  ["rectangle", ({ width, height }) => width * height],
  ["circle", ({ radius }) => Math.PI * radius ** 2])

console.log(area({ "kind": "square", "size": 10 }))
// => 100

Unfortunately, the typechecker suffers from a kind of death by thousand cuts of missing features here, and while it's almost possible to implement a type definition for match that makes this all work, we're not quite there yet. I've tried to point out the missing features in #19800.

@pelotom
Copy link

pelotom commented Nov 8, 2017

Just saw this issue and thought I'd put in a plug:

https://github.com/pelotom/unionize

You can have pattern matching today!

@xogeny
Copy link

xogeny commented Nov 8, 2017

@pelotom That looks very cool. I really like the design. I try to use the tagged union approach whenever I can and you seem to have really cleared out the boilerplate for most use cases. I'll definitely check it out!

@masaeedu
Copy link
Contributor

masaeedu commented Nov 8, 2017

One aspect that's important for pattern matching is to not force the requirement of providing an explicit discriminator onto the input data; it should be possible to pattern match on data in its "natural", messy state.

This is important to be able to get all the benefits of pattern matching from other languages (e.g. pattern matching over x:xs in Haskell should map to matching over ([x, ...y]) => in JS. This isn't possible if pattern matching is restricted to a single discriminator property.

E.g. for the shapes thing above, it should be possible to do:

const hasProp = (o, p) => o.hasOwnProperty(p)
const area = match(
  x => hasProp(x, "size") ? "square" : hasProp(x, "radius") ? "circle" : "rectangle",

  ["square",    ({ size })          => Math.pow(size, 2)],
  ["rectangle", ({ width, height }) => width * height],
  ["circle",    ({ radius })        => Math.PI * Math.pow(radius, 2)])

And then be able to match over "raw" data that is not explicitly tagged or prepped for pattern matching:

console.log(
  area({ size: 10 }),
  area({ width: 10, height: 11 }),
  area({ radius: 1 / Math.sqrt(Math.PI) }))
// => 100, 110, 1

This works in JS, so the goal in TypeScript should be to support this with strict type checking and good inference.

@AlexGalays
Copy link

Yeah, jumping through hoops just to emulate pattern matching for the one feature that is already very well supported by the compiler (discriminated unions + control flow analysis) is pointless as anything but an exercice. Plus, you lose the free exhaustivity check!

Without compiler support, it's not worth it. Will wait for that TC39 pattern matching proposal to go up in its stages, or for macros :D

@pelotom
Copy link

pelotom commented Nov 9, 2017

@AlexGalays

Yeah, jumping through hoops just to emulate pattern matching for the one feature that is already very well supported by the compiler (discriminated unions + control flow analysis) is pointless as anything but an exercice. Plus, you lose the free exhaustivity check!

I’m not sure if you’re referring to Unionize, but it’s not “emulating” pattern matching; it’s using every bit of the existing “very well supported” facility that the compiler provides. And it does exhaustivity checking!

@AlexGalays
Copy link

@pelotom Yes I was referring to it indirectly, along with a few other implementations I've seen.

If you're supporting exhaustivity, I really need to have a look... But you don't have the order-dependent + fall-through characteristic of general purpose pattern matching, right?

@pelotom
Copy link

pelotom commented Nov 9, 2017

If you're supporting exhaustivity, I really need to have a look... But you don't have the order-dependent + fall-through characteristic of general purpose pattern matching, right?

Correct, it supports the special case where you are discriminating by a unique tag and providing one case per tag, or a catchall default case to handle the leftovers (in which case you lose exhaustivity checking obv).

@xogeny
Copy link

xogeny commented Nov 9, 2017

@AlexGalays I want to jump in and comment on something. I never saw @pelotom's unionize as a replacement for full blown pattern matching capability. But there a subset of pattern matching that unionize does an excellent job of representing while eliminating lots of boilerplate. For example, I submitted a PR to the bombadil project and I can imagine that a bunch of the boilerplate there could be cleaned up very nicely with unionize. So I'd argue that while it isn't a replacement for pattern matching support at the language level, I think it is far from "pointless" because it is a huge improvement for some very common cases.

@LayZeeDK
Copy link

LayZeeDK commented Jan 13, 2018

Take a look at @ahejlsberg's take on this from Microsoft Build 2017. From around 46:42, he uses the never type to ensure that all the possible known shapes have been matched in a regular switch-case statement.

This is a lot like what @tekacs proposed.

@pelotom
Copy link

pelotom commented Jan 21, 2018

A second plug: Runtypes now supports pattern matching. Here's @masaeedu's shapes example adapted to it:

https://github.com/pelotom/runtypes/blob/a585fc813842ecd852d0ae07a4ff3673018e6c91/examples/src/shapes.ts#L11-L15

@dough654
Copy link

I know this issue has kind of gone stagnant, but I would love to show my support for this feature. Exhaustive pattern matching combined with algebraic data types is my favorite feature of F#. I don't know the specifics of implementing a feature like this in the current code base, but the syntax is so close to being what I want to work with already. I want to write code like this:

type CreditCard = {
    number: number;
    expDate: string;
    cvc: number;
}

type Check = {
    routingNumber: number
    accountNumber: number
}

type PayPal = {
    emailAddress: string
}

type PaymentMethod = CreditCard | Check | PayPal; 

type Payment = {
    Method: PaymentMethod; 
    Amount: number
}

function processPayment(payment: Payment) {
    match (payment) {
        CreditCard: cc => ...//process credit card
        Check: check => ...//process check payment
        // -> Would throw an compile time warning as not all cases have been handled
    }
}

The lightweight types and discriminated union provide a clear, concise, and powerful domain model, and the potential exhaustive pattern matching would round everything out by enforcing that domain model on the developer via the compiler. It's a system that really works very well in the F# world and it would be amazing to see something similar in TS.

It's clearly possible to transpile to javascript that enforces this, as evidenced by the existence of Fable. For now, I'm stuck using Fable basically for this functionality alone. Unfortunately, the developer experience is not as good as TS, and TS retains some of that Javascript flexibility (which I'm a fan of). In my mind, this feature alone would be enough for me to make the switch, and I doubt that I'm the only one.

Thanks for the consideration.

@just-chillin
Copy link

What about this ECMAScript proposal? There's a couple implementations of it and the syntax looks like it would fit well. I think this shows that it's at least possible.
https://github.com/tc39/proposal-pattern-matching

@samhh
Copy link

samhh commented May 1, 2020

Very likely there's no movement on this issue until that proposal reaches stage 3.

Edit: I don't much like it either folks, but check TypeScript's project goals and its past behaviour, there's zero chance of a massive break from the standards on this, and implementing a standard too early has already backfired once.

@Lonli-Lokli
Copy link

Is there any proposal\issue on ternary operator, like

return anyArray.find({condition}) let item ? computation_over(item.{prop}) : undefined?

@RyanCavanaugh RyanCavanaugh added Out of Scope This idea sits outside of the TypeScript language design constraints and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jul 21, 2020
@RyanCavanaugh
Copy link
Member

Pursuant to our goals of a) not adding type-directed emit and b) not getting ahead of TC39 on runtime features like this, I'm going to close this as out of scope for the time being. When https://github.com/tc39/proposal-pattern-matching gets to Stage 3 we'll implement this ASAP, but no sooner. Further discussion on how that feature should work belongs entirely in the proposal repo, so I'll be locking this to better channel discussion to the correct place. Thanks!

@microsoft microsoft locked as resolved and limited conversation to collaborators Jul 21, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests