5 ms·
Explanation: A record is a set of key-value pairs with the value having a given type (also called a struct, object, etc. depending on the context). In TypeScri
by arxanas 5y ago
Explanation:
A record is a set of key-value pairs with the value having a given type (also called a struct, object, etc. depending on the context). In TypeScript, this is an example of a record:
const foo = { bar: 1, baz: "qux" };
Destructuring is an ergonomic feature where you can take the fields of a record and assign them to local variables. For example:
// Equivalent to
// const bar = foo.bar;
// const baz = foo.baz;
const { bar, baz } = foo;
Besides ergonomics, destructuring can offer features like exhaustiveness-checking, to make sure that you used all of the fields of a record.
A union type is a type which denotes that the value is one of two different types, indicated like this:
type Foo = number | string;
Sometimes, you have two different cases with the same type, and you want to distinguish between them. For example:
// Not helpful -- we can't tell from the value
// whether it's supposed to be a username or an email.
type UsernameOrEmail = string | string;
To handle this case, you can use a discriminated union instead. These unions have a special value which unambiguously indicates which case is present:
type UsernameOrEmail = { type: "username", value: string }
| { type: "email", value: string };
Sometimes, you have a discriminated union with different fields:
type ApiRequest = { type: "message", subject: string, contents: string }
| { type: "view-posts", user_id: number };
Currently, TypeScript uses control-flow analysis to ensure that you're using the right kinds of fields depending on the discriminator value:
const request: ApiRequest = getRequest();
if (request.type === "message") {
// Okay:
console.log("Subject is", request.subject);
// Rejected by compiler:
console.log("User ID is", request.user_id);
}
However, you can't combine this feature with destructuring:
type Action = { kind: "A", payload: number }
| { kind: "B", payload: string };
const { kind, payload } = getAction();
if (kind === "B") {
// Not currently allowed:
payload.toUpperCase();
}
With this pull request, you can do the above. This can be convenient with patterns like reducers (see https://github.com/microsoft/TypeScript/issues/46143 https://github.com/microsoft/TypeScript/issues/46143 for an example).
- dekerta 5y agoThanks for the explanation. I've been programming for many years, and your examples make total sense to me, but reading sentences like "Control flow analysis for destructured discriminated unions" make me feel like a total impostor
- coolgoose 5y agoYou and me both buddy, you and me.
- dgritsko 5y agoI think most of us would feel like impostors next to Anders Hejlsberg.
- codyb 5y agoIt’s just like any other programming problem, break it into parts, and build it back up again. “What’s a union?” “Okay… then what’s a discriminated union?” “Okay… let’s apply destructuring to one…” “Okay… now what would analysis look like there given what I know?” “Okay… how do we have the computer know what I know intuitively as a human given this context?” “Okay! I think I got it!” :-). If you’ve gotten this far with programming to understand the examples above, my guess is if you just take the time you can do it without the examples if you take it in small enough chunks :-). Life’s all about what you apply your energy too (And also, wonderful example :-) )
- debaserab2 5y agoI know you're entirely right, but sometimes when I see things like this in Typescript... https://github.com/sindresorhus/type-fest/blob/main/source/require-at-least-one.d.ts#L25-L35 https://github.com/sindresorhus/type-fest/blob/main/source/r... I can walk through it with enough pause, but it kinda makes my head implode when I glance at it.
- JZumun 5y ago{ [Key in KeysType]-?: ... } TIL that you can disable ? and readonly in mapped types using -. So in the above code, every Key will be mandatory (-?) even if the original one in KeysType was optional. source: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers https://www.typescriptlang.org/docs/handbook/2/mapped-types....
- codyb 5y ago
- friedman23 5y agoThank you for the explanation, very clear and concise.
- jb_s 5y agoUnion types seem a bit wacky, are they typically used over function overloading or generics?
- exdsq 5y agoThe most common thing to use them for in TS is to acknowledge a value could be of type null or undefined. Let’s you then check you’re handling them correctly.
- AgentME 5y agoUnion types let you accomplish very similar things to what you can do in languages that support Enums with associated values, like Rust. (See https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html for examples of what that looks like. There's no great direct comparison in Java, because Java Enums don't support associated values like this; the closest Java pattern would involve making multiple classes that each extend an interface or abstract class.)
- Zababa 5y agoAn example of union types: many API in JS accept HTTP verbs as strings. With union types, you can say something like: type HttpVerb = "GET" | "POST" | "PUT" | "DELETE"; But you can also do more complex stuff: type User = {name: string; password: strings} | OAuthID; By themselves they aren't hard to understand or use. As an aside, compared to sum types, you don't have to define them before using them: a function can return number | null while in language with sum types you usually define option first and then return option<number>.
- arxanas 5y agoYou might find my blog post on some of the uses interesting: https://blog.waleedkhan.name/union-vs-sum-types/ https://blog.waleedkhan.name/union-vs-sum-types/ In a language like TypeScript, they can be used for function overloading since there is no runtime dispatch based on argument type. But usually they're used as sum types in the same way as e.g. Rust enums. Typescript supports so-called "singleton types", in which a given type is inhabited by exactly one value. This lets you do things like specify not just `string` but specific values like `"foo" | "bar"` as a type. In these cases, I often use them in combination with Typescript's "mapped types", which let you index into a map with a certain type and get another type out. This can be useful for things like RPC calls, where the type of the output depends on the type of the input.
- Zababa 5y agoThat's a great explanation, thank you for that.