[0] https://www.typescriptlang.org/docs/handbook/utility-types.h...
[0] https://www.typescriptlang.org/docs/handbook/utility-types.h...
Pick is also really useful if you have an interface that passes down a subset of complex things you get from a library; no need to retype their types, just extend a Pick of the library type.
In short, utility types are awesome!
I'm surprised I rarely see these in courses/tutorial. These should be like day 1 material.
type Ensure<T, K extends keyof T> = T & { [U in keyof Pick<T, K>]-?: T[U] };
class A {
foo?: number;
bar?: number;
baz?: number;
}
type MandatoryFields = "foo" | "baz";
type B = Ensure<A, MandatoryFields>;
const b: B = { foo: 42 };
Here, ts will complain that b is missing baz.The other thing is that types are more often used than read. You don't need to read the `MandatoryFields` type definition often, because your IDE/typechecker will automatically enforce the contract and tell you when you're missing properties.
Another reason could be a generic interface. If you have a lifecycle where a type is mutable at one point but immutable at later points, you could use mapped types to enforce those constraints on the class methods generically.
[0]: https://www.typescriptlang.org/play?#code/MYGwhgzhAEAKCmAnCB...
So: FormElement<string|undefined> needs to have a "disabled" function, indicating conditions under which it becomes disabled (and absent from the model), while FormElement<string> must not have a disabled function, as it will always be present in the model.
One pitfall of this approach is it requires a lot of trial and error to find the incantation that both works and doesn't swallow error messages.
Even though the code may seem inscrutable, note that the resulting type is fairly easy to understand in your IDE. That is, if you hover over the "B" to see what the type definition is, you see:
type B = A & {
foo: number;
baz: number;
}
If you defined the type like this (which is equivalent to extending the class as you were proposing) and later on someone changes the type of one such mandatory properties in the base and/or extended class without changing the other, the error becomes much much weird, on the lines of:> Type 'number' is not assignable to type 'never'.(2322)
Here typescript is saying that a prop cannot have a value (type never) because the base class defines it as "number?" but the extended one defines it as "string", and the intersection between them is empty. This is hard to understand when it pops out where you don't expect it. Harder than ignoring the weird "Ensure" thing, seeing what it does (the resulting type B definition) and moving on.
Defining advanced types may be cumbersome, but dealing with code that uses them is still approachable. This allows the more experienced team members to "shape the ground" and less experienced members still reap the benefits even if they don't fully understand how the thing works.
The advanced programmer simply writes "type Ensure<T, K extends keyof T> = T & { [U in keyof Pick<T, K>]-?: T[U] };", thus removing the need to copy the property.
The master programmer copies the property definition.
The beginner programmer copy/pastes the code.
The advanced programmer writes a function, thus removing the need to maintain copies of the code.
The master programmer copy/pastes the code.
It is not like "Ensure" would be a single-use thing. "Ensure" here is a utility type definition, which is what a "function in the world of types" would be.
> Without arguments, your comment has as much substance as me replying:
Yes, what you substituted is equivalent, and they likely could have written that to the exact same effect.
> It is not like "Ensure" would be a single-use thing. "Ensure" here is a utility type definition, which is what a "function in the world of types" would be.
There's a few ways to interpret the stanza. The way I interpreted it is that the beginner uses a library to provide the definitions, the advanced programmer just writes their own definitions inline as needed, and the master programmer uses a library for the definitions they need (whether written by themself or someone else).
In that respect, I think you're both in agreement.
For what it's worth, I think comments like these are generally beneficial, if maybe I prefer at least a line of context. That they can be interpreted differently and may require a bit of thought to map onto the current context can sometimes allow people to view their beliefs from a slight remove where more introspection is possible, or spur interesting tangents to explore. Both of those are generally beneficial in a forum like this, IMO.
I'm not sure how often you would need type helpers in application code, for frameworks and libraries they are a godsend.
Your argument is a bit like saying we don't need parameterized types like Array<T> because you can just copy paste the code for each type.
type Ensure
I define a type called Ensure <T, K extends keyof T>
This type takes two type parameters, one called T and the other K which will consist of Keys belonging to the type T (in our case, "foo", "bar" or "baz"). = T &
This new type (called Ensure) will be equal to the union of two types: One will be T and the other will be: { [U in keyof Pick<T, K>]
A new type which keys will be picked among the key listed in K -?
To which we will remove the potential optional qualifier : T[U] };
And which types will be the same as in T.In practice you'd have someone that understands this set it up once, and then document its usage for others, maybe document the implementation to make it easier to modify later.
The TS compiler is surprisingly good at giving you good readable error messages as well when your code violates these advanced types; the errors tell you what you specified and what is supported, it doesn't display the low level type logic as part of the error users see. This means that there's very little need for anyone to really how these type definitions work.
EDIT: clarifications and spelling.
I've used types like this in a pretty advanced TypeScript UI project consuming lots of services to enforce compile time errors. We were using generated TS clients for all of the APIs we consumed, and the compiler would automatically throw readable errors wherever we were missing form fields or types became incompatible. I committed the advanced type once, documented its usage, and I don't think anyone has had to deal with it since, whilst the types have steadily prevented errors.
And even then: it's just type definitions. If it really becomes a maintenance burden or someone has no clue what it does, you can simply replace the type with "any" or something similar and all of your problems are gone and typescript won't complain anymore (at the expense of less type error checking).
EDIT: improved wording
In practice, that someone then leaves the company, leaving this nightmare underfoot.
> The TS compiler is surprisingly good at giving you good readable error messages as well when your code violates these advanced types
Only if you're that original person who understands it! I would still have no idea what is happening, no matter how clear.
class B {
foo: number;
bar?: number;
baz: number;
}
I know these are toy examples, and I'm sure there are reasons why in the real world you'll be modeling things where this is not a good option. But I'd really need to be convinced that the CRUD webapps a lot of us write actually benefit from having this in our source code. "Type '{ foo: number; }' is not assignable to type 'B'.
Property 'baz' is missing in type '{ foo: number; }' but required in type '{ foo: number; baz: number; }'."
I've had the compiler emit errors much like the following for way more complicated types that combined several of these kinds of structures together to form much more bespoke type checks (reproduced from memory, so I'm not 100% certain on the error or use case): const e = form.email
^
ERROR: "email" is not in '"name" | "firstname" | "lastname" | "e-mail" | "birthdate" | "password"'
The thing to note here is that it often doesn't expose the details of the implementing type and underlying (admittedly complicated) type system primitives at all to users. That said, I'll have to be honest and say that I have seen it throw much more difficult to understand nested errors referring to the underlying type implementation when I was working on the type system itself to create stricter type checks for functionality that was previously unchecked (i.e. treated as "any" by the compiler).The other thing to note is that these things are really only doing type checking. If it becomes troublesome and it does start to spit out type errors incorrectly, throw unreadable errors, or otherwise become a maintenance burden, these types are not particularly difficult to remove, and by removing them you won't break your code. Consider that to be the equivalent of removing a linting rule or no longer requesting a review from a colleague. Though it's probably a good idea to document how to remove these advanced checks for when people find them annoying when someone leaves ;)
Incorrect type checking implementation is probably the biggest problem with these things getting complex, though. If your type check is incorrectly throwing errors for implementations that don't contain any errors at all, that's going to set you back a lot!
You mean intersection, right?
EDIT: link to docs on intersection types: https://www.typescriptlang.org/docs/handbook/2/objects.html#...
Wouldn't Required suffice here as well for this as opposed to removing the optional qualifier?
Me, reading this example: oh no, those all need to be added to our project's lint rules to make sure no-one uses them.
I never put it in production, partially because of concerns over maintainability but far more because I had no need for it.
This is overkill that will only confuse everyone who isn't the original author.
If you think it will have more special fields like completed and createdAt that you don't want to pass on in most contexts, then it's better to list the fields that you want to pass on.
But if you expect it to not have more of those and rather get (or loose) more fields that you want to pass on, then it's better to list the ones that you want to omit.
Another trade-off might be that when the new field is added and you'll forget to update the other pieces of code, will it do less harm to pass the field you intended to omit, or to omit the field you intended to pass. Or which error will be easier for you to discover in your use case.