I followed the whole throws conversation on github and I'm pretty disappointed with the arguments for not implementing it: 1) Java has it and people use it wrong - Java doesn't have inference, leading to many functions mindlessly having `throws Exception` by default. That wouldn't happen in TS. 2) You can always get exceptions which are not declared - Irrelevant. Those are always a possibility. Having domain errors in the API would be useful even with no guarantees. 3) Nobody uses the @throws in jsdoc - No popular tool make use of it when you use it. If using it provides no value, people won't use it. That's a flawed metric. 4) You can always return domain errors as values - Not many libraries do it and those that do are not very ergonomic. Go does it, and error handling is all over the code; there's no concept of "happy path". Rust does it great, but it relies on a Result type supported at the language level, with the ? short-circuiting a function like a throw does in JS. It also relies on the Result type itself and the ability to pattern-match against it. JS has nothing close to that. JS throws. Node, deno, bun, browsers, popular libraries and frameworks… they all throw. Not having a way to encode that is such a weird omission! (forgive me if I'm misremembering something)
I feel like this is already supported by putting @throws in the comment. But a linting warning if you don't handle a function with that in the comment would be pretty cool
I've proposed the interval types. The reason why I think negated types aren't a thing is because that would make some cases of resolving types extremely hard. Consider the cases where you'd union or intersect a negated type. It can result in a large type pretty quickly.
@@nisabmohd It's just a primitive type with its own built-in properties and methods. It's not that odd to think TS could include these things to even manipulate the prototype of these objects when you pass them along. So my code would be changed a tiny bit to `Omit` - capitalised the String part. The reason I'm saying this is that it would be intuitive to work with and not so arcane as many other parts of TS already are.
@@mahadevovnlYeah but TS doesn't really deal with manipulating the properties of builtins, for one. For two, your proposal ABSOLUTELY would not do what you think: it'd "remove" the "click" method from all strings of this type, if it existed, which it doesn't. `Exclude` makes sense with the way strings are treated too, because "string" isn't really an object prototype, it's more of "a union of all possible builtin strings".
@@ilonachan Fair points :) I guess it just makes a good case for validators like Zod, which is probably where this kind of thing belongs anyway, not in TS.
The Opaque type is something I wish was easier to do in C++. The relative vs absolute path is a great example, but I've also wanted things like "both position and velocity are vectors, but their arithmetic is constrained". Very educational to see how this problem is addressed in Typescript, thank you!
5:55 is such a great pattern for when you need extra protection around a set of exported functions. Form validation & sanitization comes to mind. All of the external functions accept only the branded type and a provided gatekeeper/sanitize function returns the input in the correct branded type.
Wouldn't that allow you to pass in any primitive except other nominal types? As in, if u have: type A = string & {[_brand]?: "A" } And then you have a function that accepts A, wouldn't you be able to pass in any string, except other nominal types that aren't A
@@specy_ You're right, this allows for that kind of type conversion. However, I think it's less destructive than using as-casting, as it doesn't disable any type checks. I usually use brand types as ID types, as sometimes the ID can represent different types of entities in different contexts. Therefore, it's convenient to have the ability to cast between a brand type and a string, and vice versa, without the need for as-casting. However, this scenario is relatively rare. More often, I pass many IDs into a function, and this ensures that I don't accidentally pass the wrong ID.
Nominal types are also referred to as "newtypes" in other languages. The library "gcanti/newtype-ts" provides a cool implementation that you can use :D
matt hi, this one of best channels about advanced ts, great content. Can you make a video about project references? how to use them? implications? how to work with them when we are using bundlers ? pros vs contras. I'm moving one large monorepo to ts project references, but i found i have to make a build each time i wanna typecheck code and that is generating slow performance, even if its just declaration files, but is taking so much
hi matt, i ve been doing this for a month. and I faced some problems migrating to turborepo. do you have any example/video working with turborepo and lefthook (or lintstage). I want to execute tasks in precommit just for workspaces that were affected and are staged. but is kind of impossible using turborepo by itself. The unique way that ive seen is using --filter=[HEAD] but im even matching not staged files (workspaces). Thanks
IMPORTANT FOR MATT Hi Matt. You, as a TS ninja, whose opinion on TS is important worldwide, can affect TS team on the following issue :) Or maybe you can explain why stuff that I will describe below is as it is: For example I declared type: type MyType = { name: string values: number[] } const myObject: MyType = { name: "Batman" values: [3, 5, 8] }of Why TS allows to doubt here: myObject.values?.filter(num => num > 10) Why it allows to use this question mark? We definitely know that values is mandatory, and they are represented by array. It should say something like - hey man, this question mark is redundant. Please, help to understand why it works like this Kind regards
Yes, TS currently needs to read every file to detect global augmentations, and will do for as long as global augmentation is allowed in TS. So multi-threadedness has limited benefits.
I've seen examples of branded types from your LinkedIn as well, and I don't understand what's the point of this? If we are talking about the example with relative and absolute paths, I find it completely useless it still does not check a string whether it is a relative path or an absolute one, and it doesn't check it in compile time nor runtime. If I would need something similar, I would just go with wrapping a string into a class with putting validation to the constructor
5:35 There is another way of doing type branding without using `as`: declare const Brand: unique symbol declare const AbsolutePathBrand: unique symbol declare const RelativePathBrand: unique symbol type AbsolutePath = string & { [Brand]?: typeof AbsolutePathBrand } type RelativePath = string & { [Brand]?: typeof RelativePathBrand } const absolutePath: AbsolutePath = "/path/to/file" const relativePath: RelativePath = "../relative/path" const stringPath = "string" declare function handleAbsolutePath(path: AbsolutePath): void handleAbsolutePath(absolutePath) // ok handleAbsolutePath(relativePath) // error handleAbsolutePath(stringPath) // ok handleAbsolutePath("hi") // ok declare function handleAbsolutePath2(path: typeof Brand extends keyof P ? P : never): void handleAbsolutePath2(absolutePath) // ok handleAbsolutePath2(relativePath) // error handleAbsolutePath2(stringPath) // error handleAbsolutePath2("hi") // error
I've been checking out your courses, I did all the free ones because I liked them so much and when I go to the checkout to buy one. $200?! $500?! is too much dude, I'm not saying your content isn't worth it but the market is full of typescript or javascript courses at lower prices. Believe me, lower the price to $25 - $80 and you'll have more sales (including me)
Because why wouldn't you want to know if the key isn't omitting anything (other than the exception given in the video)? The vast majority of Omit's use cases are "omit this field from this object"
Yes, but they wouldn't be cross-platform, and it also doesn't work well with external libraries. In my repo I end up casting the results of third-party libs like glob.
I followed the whole throws conversation on github and I'm pretty disappointed with the arguments for not implementing it:
1) Java has it and people use it wrong - Java doesn't have inference, leading to many functions mindlessly having `throws Exception` by default. That wouldn't happen in TS.
2) You can always get exceptions which are not declared - Irrelevant. Those are always a possibility. Having domain errors in the API would be useful even with no guarantees.
3) Nobody uses the @throws in jsdoc - No popular tool make use of it when you use it. If using it provides no value, people won't use it. That's a flawed metric.
4) You can always return domain errors as values - Not many libraries do it and those that do are not very ergonomic. Go does it, and error handling is all over the code; there's no concept of "happy path". Rust does it great, but it relies on a Result type supported at the language level, with the ? short-circuiting a function like a throw does in JS. It also relies on the Result type itself and the ability to pattern-match against it. JS has nothing close to that.
JS throws. Node, deno, bun, browsers, popular libraries and frameworks… they all throw. Not having a way to encode that is such a weird omission!
(forgive me if I'm misremembering something)
Something indicating that a function can throw an error would be nice, so you'd know to catch it
I feel like this is already supported by putting @throws in the comment. But a linting warning if you don't handle a function with that in the comment would be pretty cool
That is doable in user land. Fp-ts, effect and several other libraries does
Errors as values is the way
@@TheBswan that's what I came with eventually, to avoid throwing errors and just return an object with error code
This is literally the only update I want from typescript team
I've proposed the interval types. The reason why I think negated types aren't a thing is because that would make some cases of resolving types extremely hard. Consider the cases where you'd union or intersect a negated type. It can result in a large type pretty quickly.
I wish they would just allow for `Omit` and that's it. That reads intuitively: it can be any string but not 'click'.
Omit is for object types 👍
`Exclude` would make more sense than Omit. It already works as you expect on unions: `Exclude // => 'touch'`
@@nisabmohd It's just a primitive type with its own built-in properties and methods. It's not that odd to think TS could include these things to even manipulate the prototype of these objects when you pass them along.
So my code would be changed a tiny bit to `Omit` - capitalised the String part.
The reason I'm saying this is that it would be intuitive to work with and not so arcane as many other parts of TS already are.
@@mahadevovnlYeah but TS doesn't really deal with manipulating the properties of builtins, for one. For two, your proposal ABSOLUTELY would not do what you think: it'd "remove" the "click" method from all strings of this type, if it existed, which it doesn't.
`Exclude` makes sense with the way strings are treated too, because "string" isn't really an object prototype, it's more of "a union of all possible builtin strings".
@@ilonachan Fair points :) I guess it just makes a good case for validators like Zod, which is probably where this kind of thing belongs anyway, not in TS.
The Opaque type is something I wish was easier to do in C++. The relative vs absolute path is a great example, but I've also wanted things like "both position and velocity are vectors, but their arithmetic is constrained". Very educational to see how this problem is addressed in Typescript, thank you!
5:55 is such a great pattern for when you need extra protection around a set of exported functions. Form validation & sanitization comes to mind.
All of the external functions accept only the branded type and a provided gatekeeper/sanitize function returns the input in the correct branded type.
Brand types can be applied without casting, if brand property is optional
Nice!
Wouldn't that allow you to pass in any primitive except other nominal types?
As in, if u have:
type A = string & {[_brand]?: "A" }
And then you have a function that accepts A, wouldn't you be able to pass in any string, except other nominal types that aren't A
@@specy_ You're right, this allows for that kind of type conversion. However, I think it's less destructive than using as-casting, as it doesn't disable any type checks.
I usually use brand types as ID types, as sometimes the ID can represent different types of entities in different contexts. Therefore, it's convenient to have the ability to cast between a brand type and a string, and vice versa, without the need for as-casting.
However, this scenario is relatively rare. More often, I pass many IDs into a function, and this ensures that I don't accidentally pass the wrong ID.
@@munzamt I mean it's better than nothing I guess
Typescript does have nominal types for classes with private members, you can also use those for branding
I think I can hear the rain at the end of the video, anyway thanks for the awesome vid Matt !
You forgot the most important one, why they do not type Object.keys for narrowed types and just make them string. 😉
Yeah, this is a result of not having exact object types by default.
I thought you'd at least list some of the things you fixed with ts-reset :P
Those are small-fry! Opaque types would be so sweet
Coming from Scala 3, I really like opaque types.
Nominal types are also referred to as "newtypes" in other languages. The library "gcanti/newtype-ts" provides a cool implementation that you can use :D
I love the structural type system of TS, but I've hoping for nominal types since the day I found out about them
matt hi, this one of best channels about advanced ts, great content. Can you make a video about project references? how to use them? implications? how to work with them when we are using bundlers ? pros vs contras. I'm moving one large monorepo to ts project references, but i found i have to make a build each time i wanna typecheck code and that is generating slow performance, even if its just declaration files, but is taking so much
hi matt, i ve been doing this for a month. and I faced some problems migrating to turborepo. do you have any example/video working with turborepo and lefthook (or lintstage). I want to execute tasks in precommit just for workspaces that were affected and are staged. but is kind of impossible using turborepo by itself. The unique way that ive seen is using --filter=[HEAD] but im even matching not staged files (workspaces). Thanks
Not only do I have to program JS in TS every day, but now I also have to program the Typings for TS. Stop the torture :(
IMPORTANT FOR MATT
Hi Matt. You, as a TS ninja, whose opinion on TS is important worldwide, can affect TS team on the following issue :) Or maybe you can explain why stuff that I will describe below is as it is:
For example I declared type:
type MyType = {
name: string
values: number[]
}
const myObject: MyType = {
name: "Batman"
values: [3, 5, 8]
}of
Why TS allows to doubt here:
myObject.values?.filter(num => num > 10)
Why it allows to use this question mark? We definitely know that values is mandatory, and they are represented by array. It should say something like - hey man, this question mark is redundant.
Please, help to understand why it works like this
Kind regards
TS lets you do unnecessary checks. ESLint can help prevent that.
Also, I have zero influence over the TS team
@@mattpocockuk I know, but I am sure they respect you. Thank you very much
The TypeScript God is trying to tell you something Matt
2:44 type Spread = T2 & BetterOmit;
awesome video!
5 features the enzo checker already has
What about Symbols? Symbols are something unique, but it seems typescript doesn't really know that.
Which also makes symbols prime candidates for the hacky nominal typing workaround
brand type assertion awesome !
Another thing that is unlikely to be implemented is a multi-threaded tsc. Yes, I know it's not a language feature, but still.
Yes, TS currently needs to read every file to detect global augmentations, and will do for as long as global augmentation is allowed in TS. So multi-threadedness has limited benefits.
I was hoping your book would be an actual paper book that I could buy.
It will be! Published by No Starch
`Exclude` should work no ?
Nope, that's just string
I've seen examples of branded types from your LinkedIn as well, and I don't understand what's the point of this? If we are talking about the example with relative and absolute paths, I find it completely useless it still does not check a string whether it is a relative path or an absolute one, and it doesn't check it in compile time nor runtime. If I would need something similar, I would just go with wrapping a string into a class with putting validation to the constructor
thaamks peacock 🦚
Shout out to the TypeScript Tuesdays emails! If you're not subscribed, you should be!
It saddens me that we can't just
type NewSpeak = Exclude;
5:35 There is another way of doing type branding without using `as`:
declare const Brand: unique symbol
declare const AbsolutePathBrand: unique symbol
declare const RelativePathBrand: unique symbol
type AbsolutePath = string & { [Brand]?: typeof AbsolutePathBrand }
type RelativePath = string & { [Brand]?: typeof RelativePathBrand }
const absolutePath: AbsolutePath = "/path/to/file"
const relativePath: RelativePath = "../relative/path"
const stringPath = "string"
declare function handleAbsolutePath(path: AbsolutePath): void
handleAbsolutePath(absolutePath) // ok
handleAbsolutePath(relativePath) // error
handleAbsolutePath(stringPath) // ok
handleAbsolutePath("hi") // ok
declare function handleAbsolutePath2(path: typeof Brand extends keyof P ? P : never): void
handleAbsolutePath2(absolutePath) // ok
handleAbsolutePath2(relativePath) // error
handleAbsolutePath2(stringPath) // error
handleAbsolutePath2("hi") // error
This will accept strings
@@mattpocockuk nongeneric version only. I've checked in playground. Generic version (handleAbsolutePath2) rejects strings.
For the branded types you could use template literal types
In this example, the branded type is the string type. What about other types? Can the template literals brand those types?
Think of an instance from class what about them
I've been checking out your courses, I did all the free ones because I liked them so much and when I go to the checkout to buy one. $200?! $500?! is too much dude, I'm not saying your content isn't worth it but the market is full of typescript or javascript courses at lower prices.
Believe me, lower the price to $25 - $80 and you'll have more sales (including me)
Why would I expect Omit to throw errors when the keys don't overlap? It wouldn't make any sense.
Because why wouldn't you want to know if the key isn't omitting anything (other than the exception given in the video)? The vast majority of Omit's use cases are "omit this field from this object"
@@ThisAintMyGithub not at all my experience. I very often omit via unions.
Can't AbsolutePath/RelativePath be defined like type AbsolutePath = `/${string}` and type RelativePath = `.${string}`?
Yes, but they wouldn't be cross-platform, and it also doesn't work well with external libraries. In my repo I end up casting the results of third-party libs like glob.
Relative paths don't necessarily start with `.` either.
@@auscompgeek True, this would be a kind of crude solution that might work in certain cases, not in all.
5:55 very gross indeed
If u wanna do absolute/relative path without branding: T extends `../${string}` ? never : T
This doesn't make sense when most of the types are 'string'