Make Primitive Types Incompatible using Typescript Structural Typing

Tomasz Ducin
InstructorTomasz Ducin
Share this video with your friends

Social Share Links

Send Tweet
Published 9 months ago
Updated 5 months ago

Structural typing is when TypeScript looks at the shape of a type when comparing two different types. We can take advantage of this pattern by extending our type alias with a structural type that makes a primitive number type incompatible with our alias.

We'll explore how to work with structural types in this lesson!

[00:00] We can slightly modify the way TypeScript evaluates type compatibility across various types using TypeScript structural typing characteristics. That means if we want to change the type compatibility, we need to change the shape of a certain type by, for instance, extending the primitive with [00:19] properties. At first, this could sound slightly strange, but we have already mentioned that a primitive is going to be boxed automatically wrapped into an object if only JavaScript requires that. The whole point of what we're doing is that this information, this additional property is going to be [00:39] compile time only. This is not going to exist during JavaScript's run time. So just to illustrate how it works, let's declare 2 variables. 1 is going to be of type money, m for money. And let's also declare the n, which stands for number. What we want to [00:59] see is whether we can assign one to the other and whether it works or not. So we can see that money can be assigned into number, but not the other way around. Why is that? Each expression of type money satisfies 2 conditions. 1st, it's number primitive. 2nd, it [01:18] includes a property type of some unknown value, whatever. But on the other hand, number satisfies only one condition, which is basically being a number. So when we have a variable of type, an expression of type, money, then the requirement of being simply a number is [01:38] satisfied. Well, it additionally has some additional properties, but it's not relevant. The only thing that is required, and it's being a primitive, is satisfied so that it works. On the other hand, when we want to assign anything into the money type, then 2 conditions need to be satisfied. Both being the primitive and having the [01:58] additional property. And our number primitive satisfies the first condition, but it doesn't satisfy the other condition. In other words, we can think of types as being contracts, including 1 or more different conditions to be met. And this is how TypeScript evaluates type compatibility. [02:18] Now, it would be possible theoretically to add this type property with the unknown value into a certain object. So just to make it slightly more bulletproof, let's just provide it with a unique symbol so that it would be impossible from the outside to provide exactly [02:38] this symbol, this is because of how symbols work, and TypeScript requires us to annotate the type property with a read only modifier, so So just for the sake of correctness, let's annotate the type with a read only. But what is important is that we have made our type alias to [02:58] be compatible only one way. So that each money is a number, but not each number is a valid money value. So what can we do with such money type? Well, we can do the arithmetic operations such as add equals, and here we can [03:18] add m plus m. We can clearly see this is just a number since m itself is a number. We can also subtract and multiply, etcetera, etcetera. So we can see that due to the fact that money is a number, all of these work. However, if we want to create a money [03:38] expression, we cannot just assign anything because we're not sure whether this number value relates to money. We need to check it somehow. Generally, there are 2 directions in which we can deal with that. One would be to simply brute force the compiler by saying, hey, compiler, Don't treat [03:58] this expression as you would normally infer it to be the primitive number. Please subtype it so that it's not only a number, but it also includes one additional property. So this as money could also be wrapped into a function that accepts an argument, which is a [04:17] number, and essentially, it's going to do the very same thing, just we wrap it with a function so that whenever we want to modify the way that we verify whether something is money or not, this is essentially being wrapped in a function that we're going to reuse, but still there is no [04:37] check. However, there is a better way to deal with this.

Arti
Arti
~ 3 months ago

When is the keyword declare used as a prefix for defining a function or variable?

Tomasz Ducin
Tomasz Ducininstructor
~ 3 months ago

Hi @Arti,

WHEN

declare is something you extremely rarely use in production apps, more often library maintainers include it in lib internal types.

But it's very convenient to learn/experiment with TypeScript using the declare keyword.

WHY

Sometimes you just want to try things out, e.g. see how compatible is X with Y. And for a specific thing to play with, you first need to create it, e.g. by creating a real object or a real function, e.g.

type Something = string | number
var s: something = 5
s // number, NOT string

now the issue above is that you wanted to have string | number but TS isn't stupid :P and it can see it has to be a number. What you could do is:

var s: something = 5 as string | number

but... (1) it's fragile (if the Something type changes elewhere), (2) prone to errors and (3) longer than if you just wrote:

declare var s: number | string

and that's it :).

HOW

bear in mind that declare makes it visible ONLY IN COMPILE-TIME and the runtime equivalent doesn't exist at all. It makes sense to use this in production only when some OTHER JS script has created e.g. a global JS object which your local TS couldn't figure out, but you need to access it, so here you go with declare - but mostly it's for libraries only.

Markdown supported.
Become a member to join the discussionEnroll Today