5 ms·
Very nice! The very, very related thing I found myself wishing I had just today is the ability to destructure discriminated union fields which only exist on on
by 015a 5y ago
Very nice!
The very, very related thing I found myself wishing I had just today is the ability to destructure discriminated union fields which only exist on one component type of the union.
As an example, with the following type system:
type Output = OutputExists | OutputDoesntExist;
interface OutputExists { exists: true, url: string };
interface OutputDoesntExist { exists: false };
The ability to destructure like:
const f = ({ exists, url }: Output) => {}
Such that, at the time of destructuring:
exists: true | false
url: string | undefined
I believe, with this PR, if we re-structured the types above to:
interface OutputDoesntExist { exists: false, url: undefined };
That destructuring would be possible, with all the control flow analysis necessary to make it useful. Today, even that wouldn't be possible, so definitely excited for this change.
But, I'm curious if anyone knows if this change can extend to situations where a field isn't even defined in one of the unions, and the compiler can just insert an undefined type for those fields. Or, maybe this would have some unresolveable side-effects I'm not considering.
- gherkinnn 5y agoI repeatedly come across the same issue. So I gave it a try running TS 4.6 Nightly [0] In short: interface OutputDoesntExist { exists: false }; // error, as ever :( 0 - https://www.typescriptlang.org/play?ts=4.6.0-dev.20211103#code/C4TwDgpgBA8grsMCoF5YKcAogDwJYDOwBUAPuoggCID2EBAdtvkQFB5MQBOAZgIYBjaPErNCxKAG8oEFsQBcUYFzgQANFDhcANoqJcOAcygBfdp16DhGanUZiiUmXIKL+2gtDOsBNBo55UKAAKaVlxAg0tbVNFEUwASlQAPilvViA https://www.typescriptlang.org/play?ts=4.6.0-dev.20211103#co...
- eyelidlessness 5y agoWhat I do: interface OutputDoesntExist { exists: false; url?: never; } Now it’s: - Optional in the union type - Always present and has the correct type in the exists: true case - Still optional in the exists: false case, but if you try to access it you always get a type error
- 015a 5y agoI like the use of 'never' over just 'undefined'; really drives home that not only is it not there, but it shouldn't exist at all in that case. I'm trying to think of a reason why the compiler couldn't allow that destructuring, implicitly type the values missing in some components of the union as 'T | ... | never', then lean on the control flow analysis it looks like we're getting in 4.6 to specify the type as the discriminated union is de-discriminated (lol). JS itself does allow the destructuring of elements missing from an object, they come out as undefined, so I don't believe there'd be any underlying issues. Hopefully we see it in 4.7!
- eyelidlessness 5y agoThis is the second release in a row with improved control flow narrowing like this. If they’re not actively working on it I’d be surprised. Just a matter of physics (time). The only underlying issue I could foresee is async. Eg narrow, await, infer. But I’d assume that affects all similar cases. IMO this is as good a time as any to introduce an immutable-by-default mode. If you know it won’t change you can trust the previously narrowed type at any async boundary.
- nielsbot 5y agoDon’t think they can do anything that isn’t strictly a superset of JS. That said I think you can use `const` and `Readonly<T>` to get some safety against mutability.
- eyelidlessness 5y agoImmutable by default can still be a strict superset of JS. It just requires treating const assignments and function parameters as Readonly (or ReadonlyArray/ReadonlyMap/ReadonlySet) and introducing a Mutable type/mutable keyword. In my own personal projects I make nearly everything immutable at the type level and, while it makes my code safer it’s an enormous pain in the butt to type readonly on every property. As a matter of habit it has one other benefit though: you never have to worry about accepting `as const` values in function calls.
- deleted 5y ago[deleted]
- dgreensp 5y ago"never" is not really ever a good choice for a property/member type, because it is assignable to all types. A function that literally never returns could/should have a return type of never, for example, but if the function actually returns a value, you have a "value of type never" which is supposed be impossible, and never acts like any (because a contradiction implies anything).
- eyelidlessness 5y ago> "never" is not really ever a good choice for a property/member type, because it is assignable to all types. This isn’t right. It’s not assignable to anything other than never, that’s why never is so useful for this use case.
- deleted 5y ago[deleted]
- ljm 5y agoIs there another example than the one you provided? What I see in OutputExists/OutputNotExists is actually a Maybe/Option type. doSomething<Maybe<Output>>(someObj) .map(({ url }) => url) .getOr(null) // value could either be url or null
- 015a 5y agoYeah my example was really just an Option type. A better example: think of Kubernetes API objects. In short, expressed as typescript interfaces: type KubeObject = Pod | Service; interface Pod { kind: "Pod", metadata: Metadata, spec: { containers: Container[] } }; interface Service { kind: "Service", metadata: Metadata, spec: { selector: ServiceSelector } }; const f = ({ kind, spec: { containers, selector } }: KubeObject) => {} So, the ability to destructure both 'spec.containers' and 'spec.selector' given just a KubeObject, and have the compiler use control flow analysis on 'kind' to mark one as 'never' or 'undefined' when its destructured on a type where it doesn't make sense.
- dgreensp 5y agoI don't think the language designers would consider this, because there is a difference in TypeScript between a property existing with type undefined and not existing, regardless of the fact that it might sometimes be convenient if a non-existent property acted like a property that existed and had type undefined.
- marton78 5y agoReally? I didn't know that. That means, there is a third kind of null?
- nosianu 5y agoThe underlying object that the runtime creates in memory can have a property or not. If it has a property, that property can have no value (undefined). It can also be null, which is a value. So, you end up with two different runtime memory representations that when you query an object's property can both result in an "undefined". Cause of this is Javascript's laissez-faire approach to type safety. The implicit conversions (e.g. string+number+anyType works) are one, and not being strict about objects and their properties is another. In a strict language accessing a non-existent property would be an error and not even make it through the compiler. Somewhat unrelated to the question I responded to: Typescript is not a different language at all, it really only adds type annotations and the actual language is 100% ECMAscript apart from namespaces (no longer necessary, they are from pre-ES2015 times when JS was lacking features) or enums and maybe some minor class helper stuff nothing at all needs to be transpiled to get from Typescript to ECMAScript. You just need to remove the type annotations. That is why TS ends up with still having issues because it cannot deny the easy-going attitude of the code. Typescript is not the best architecture: It is a monolith software that supports completely different things in one giant piece of software. We used to have compiler, linker, profiler, code analysis tools, all separate tools. Typescript in one piece bundles type checking and transpiling in one, and on top of that a quite elaborate and extensive editor support (language service for editors to use). For no good reason, you can not use the transpiling feature at all and only use TS with an IDE to give type advice, and then use Babel to remove the Typescript annotations. Transpiling is really from Javascript to Javascript - working around newer ECMAScript features that a targeted older runtime does not have. I think Microsoft did a great disservice to their own product by crating this monolith, which unfortunately also created the impression that Typescript is a different language. If they had it split, and/or made that split clear, that type annotations are just added on top of whatever the latest ECMAScript is, and that transpiling is separate and does not work on a whole new language, and is only necessary when targeting platforms that don't support newer ECMAscript features used in the code, there might be less confusion about what TS is. Flow, the other type system, always made it clear it's a type-annotation add-on. TS is actually not different, they just chose to also bundle the transpiler. This monolith also makes TS a lot less flexible: If you only had type annotations and checking and transpiling separate you could have more flexibility. You can extend Babel with your own modules easily, e.g. to add a module that removes debug code in a production build. With TS you are limited to using exactly the features they implemented. This is less about the type stuff, more about the environment. For example, I have code that keeps different platform versions at once, and in the build only one is used. I have no way of telling TypeScript that. If the tool was split and as flexible as Babel it would be easy to write a tiny module or plugin.