8 ms·
The number of possible states exploding is indeed a problem with frameworks like React. One mental model that helps reduce this load, that you alluded to, is a
by bcherny 7y ago
The number of possible states exploding is indeed a problem with frameworks like React.
One mental model that helps reduce this load, that you alluded to, is avoiding product types where possible in favor of sum types.
Eg. Imaging you have a type describing React props for some component: {a?: number} and {a: number} | {} seem very similar on the surface — they both describe an object with a single field that may or may not be defined. In both cases you have 2 possible states.
But as soon as you add another field, the number of states can grow quickly unless you’re thoughtful: {a?: number, b?: string} has 4 states, while the model you might have been trying to describe — {a: number} | {b: string} — still has just two.
The less states you have, the easier it is to reason about and automatically verify your model. But in practice, people often accidentally introduce product type where they meant to use sums. I see this with Flow and TypeScript-typed React code all the time.
- danenania 7y ago"{a?: number, b?: string} has 4 states, while the model you might have been trying to describe — {a: number} | {b: string} — still has just two." The problem I've run into with the second approach as the number of states expand is 1 - you end up with tons of boilerplate to discriminate between types, and 2 - it becomes hard to treat objects polymorphically. I sometimes end up doing {a: number, b?: undefined} | {b: string, a?: undefined} so that I can write `if (obj.a) doSomething()`, but it obviously gets unwieldy with a lot of properties and states. Imo it would be nice if TS did this automatically so that I could check for the existence of any property that might be defined on an object.
- aratakareigen 7y agoI don't know TypeScript, but can't you expend a few lines to write a function 'getA : State -> number?'? Paired with the TypeScript equivalent of Swift's / Rust's if-let, it should be just as convenient.
- crdrost 7y agoRight, TS wants you to go full-on tagged union. {type: 'a', a: number} | {type: 'b', b: string} When you do this you consume it with a switch(x.type) statement, not an if() statement, so that you can write `default: assertImpossible(x.type)` for some function `assertImpossible(y: never): never` that throws an exception of your choice. This gives clear fail-at-runtime semantics if JS hands you some crap your types weren’t anticipating, but also fail-at-compile-time semantics if your pattern match is no longer exhaustive (i.e. you added a new case, now TypeScript tells you everything you need to change). In general it's really nice to view any data structure as isomorphic to the control structure that consumes it. “lists” are consumed by for..of loops, “dicts” are consumed by for..of Object.keys() loops, sum types are consumed by switches, objects are consumed by property accesses, etc.
- danenania 7y agoRight, tagged unions are what I use, but when you start having many variants, nested tagged unions, etc., it gets quite awkward imo when you need a big ol’ switch statement to get at some property or other instead of just a simple property check.
- bcherny 7y agoIf your code is fully typed, you don't need the default, since you know at compile time that you've handled your cases (Playground link: [0]): type Props = | { type: 'a', a: number } | { type: 'b', b: string } function f(props: Props): number { switch (props.type) { case 'a': return 1 case 'b': return 2 } } Unfortunately you're right that this breaks when using `if` instead of `switch` (unless you have a default return at the end of your function, or replace one of your `if`s with an `else`; Playground link: [1]): type Props = | { type: 'a', a: number } | { type: 'b', b: string } // Error TS2366: Function lacks ending return statement and return // type does not include 'undefined'. function f(props: Props): number { if (props.type === 'a') { return 3 } if (props.type === 'b') { return 4 } } This second case is a bug in TSC, and is tracked in https://github.com/microsoft/TypeScript/issues/18319 https://github.com/microsoft/TypeScript/issues/18319. [0] https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgeygZ2AXgFClAH2AG9xoYAuYAcgEMqAaYGygOwFcBbAIxkWAF9ceYIRKRYlKlwbAulNGEQBLFgHMB2ISABmbFgGMwS5C2DaAFFBTpKSVGgCUrTjz5EtoNAHclYfQAtgS2s0ADpxGAdiDzx9GjQ4WipKRBgwNkRTAGYY0DiE6mkUtIzTABZcwTxBIA https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey... [1] https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgeygZ2AXgFClAH2AG9xoYAuYAcgEMqAaYGygOwFcBbAIxkWAF9ceYIRKRYlKlwbAulNGEQBLFgHMB2ISAD024AFFEKPgBUAygCYAzADYblAGJsWAYzBLkLYABsaLgNYYMCwAJirqiDBgbIheCjRgMBzBYEyhwJHRsVrAuqSwwCHIMBgsyKkqLt5sIXBUzrUAZiowIVQAdDmNzm4eXo0AFFAo6JRIqGgAlKycPHxEOSBKjcBDI2jt4nCYO9R0k8SLoJkxXlZHgsLAy6vDE5tkWLtSVAcLVyAnscAALBc5giAA https://www.typescriptlang.org/play/#code/AQ4FwTwBwU2AFATgey...
- IshKebab 7y agoThe fundamental problem is that Typescript (and JavaScript) doesn't have tagged unions. In Rust or even C++ using std::variant this isn't a problem.
- bcherny 7y agoTypeScript does support tagged unions, actually: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions https://www.typescriptlang.org/docs/handbook/advanced-types.... As does Flow: https://flow.org/blog/2015/07/03/Disjoint-Unions/ https://flow.org/blog/2015/07/03/Disjoint-Unions/
- panzerklein 7y agoEvery language can do tagged unions with "kind" or "tag" markers and switch statement. But it's going to be verbose without pattern matching/destructuring combo.
- bcherny 7y agoIt’s not that bad, to be honest. Maybe a few more keystrokes than Haskell, but so is everything else in a C-style language. Definitely a fair point that it is a few more keystrokes, though.
- frankpf 7y agoThankfully TypeScript's type system is powerful enough (and JS's syntax flexible enough) to decently simulate pattern matching: https://gist.github.com/frankpf/cde7f792580f731dfe886ed2d91bab45 https://gist.github.com/frankpf/cde7f792580f731dfe886ed2d91b... (see the second file for usage)
- brandonhorst 7y agoSwift handles this exact problem quite elegantly with the "if case let" construct, allowing you to destructure sum types directly within an if statement. https://alisoftware.github.io/swift/pattern-matching/2016/05/16/pattern-matching-4/ https://alisoftware.github.io/swift/pattern-matching/2016/05...
- bcherny 7y agoYou can use an `in` check for this, since TypeScript version 2.7 [0]. Here's an example (Playground link: [1]): import * as React from 'react' type Props = | {a: number} | {b: string} function MyComponent(props: Props): JSX.Element { if ('a' in props) { return <div>a is {props.a}</div> } return <div>b is {props.b}</div> } let x = <MyComponent a={3} /> let y = <MyComponent b="foo" /> [0] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#type-guards-inferred-from--in-operator https://www.typescriptlang.org/docs/handbook/release-notes/t... [1] https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcA5FDvmQFA0wCeYWcACsWOgLw1x9wAfOAG9kALjgA7AK4gARligBfXvyHC5E1DCjBJAcxU0C0yfmARJcALIMAwiUiSskmAAowHVBPYROASgkAKQBlAA0AOgBRABssEBd4YVVgAjg3MmQyOD04Tz9UfxFVPkoYaSgrAB4AE2AANwA+ZBz0YXzOCOQlKoB6OqbVFVKscsq4WobGuVaRDtQIuR7+qZojOPgADzguCdsHcEtElC5hAGYlOF7Gmg24Bh29+0cj1zg5LgAiAggIT6ubkA https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLg...
- danenania 7y agoOh nice! I’ll give that a try, thanks.