# Chapter 7: Thinking in types
In our TypeScript journey, we are now at a point where we have a real good understanding of how the type system works. We started with simple types for our objects and functions. We learned more about widening and narrowing types within the type-space. And with generics and conditional types, we prepared ourselves for the unknown and modeled behavior.
These are quite some tools we have to make our JavaScript code more type-safe, more robust, and less error-prone. In this chapter, we want to strengthen our knowledge by seeing solutions to problems you might encounter every day in your TypeScript life. Our goal is to write just a couple of types to make our life easier, so we can focus on coding more JavaScript.
And along the way, we're going to learn some new concepts!
## Lesson 43: Promisify
In this lesson we use:
- Union types for function overloads
- Conditional types with *infer*
- Variadic tuple types
Once you get used to writing promises, it becomes harder and harder to go back to the old-style callback way of doing asynchronous programming. If you are like me, the first thing you'd do is to wrap all callback-style functions in a Promise and move along. For me, this has become a pattern that I use so often that I wrote my own *promisify* function. A function that takes any other callback-based function and creates a Promise-style function out of it.
This is what we expect as behaviour:
```typescript
// e.g. a function that loads a file into a stirng
declare function loadFile(
fileName: string,
cb: (result: string) => void
);
// we want to promisify this function
const loadFilePromise = promisify(loadFile)
// the promisied function takes all arguments
// except the last one and returns a Promise
// with the result
loadFilePromise('./chapter7.md')
// which is a string according to loadFile
.then(result => result.toUpperCase())
```
When thinking in types, we always start by declaring a function and making sure we know what to expect as input and output. The input is a function that can be promisified, which means that it has a callback at the end of the function. The output is a *promisified* function, which means a function that takes a number of arguments and returns a Promise.
A function with a callback as input, and a promisified function as output. There are our input and output types:
```typescript
declare function promisify<
Fun extends FunctionWithCallback
>(fun: Fun): PromisifiedFunction<Fun>;
```
Now, let's model our newly created types. `FunctionWithCallback` is somehow peculiar, as it needs to work for a potentially endless list of arguments before we reach the last one, the callback.
One way to achieve that would be a list of function overloads, where we make sure that the last argument is a callback. But this method has to end *somewhere*. The following example stops at three overloads:
```typescript
type FunctionWithCallback =
(
(
arg1: any,
cb: (result: any) => any
) => any
) |
(
(
arg1: any,
arg2: any, cb: (result: any) => any
) : any
) |
(
(
arg1: any,
arg2: any,
arg3: any,
cb: (result: any) => any
) : any
)
```
A much more flexible solution is a *variadic tuple type*. A tuple type in TypeScript is an array with the following features.
1. The length of the array is defined.
2. The type of each element is known (and does not have to be the same).
For example, this is a tuple type:
```typescript
type PersonProps = [string, number]
const [name, age]: PersonProps = ['Stefan', 37]
```
Function arguments can also be described as tuple types. For example
```typescript
declare function hello(name: string, msg: string): void;
```
Is the same as:
```typescript
declare function hello(...args: [string, string]): void;
```
A variadic tuple type is a tuple type that has the same properties โ defined length and the type of each element is known โ but where the *exact shape is yet to be defined*.
This is a perfect use case for our callback-style function. The last argument *has* to be a callback function, everything before is *yet to be defined*.
So `FunctionWithCallback` can look like this:
```typescript
type FunctionWithCallback<T extends any[] = any[]> =
(...t: [...T, (...args: any) => any]) => any;
```
First, we define a generic. We need generics to define *variadic tuple types* as we have to define the shape later on. This generic type parameter extends the `any[]`. With that we catch all tuples. We also default to `any[]` to make it easier to use. Then comes a function definition. The argument list is of type `[...T, (...args: any) => any])`. First the *variadic* part that we define through usage, then a wildcard function.
This ensures that we only pass functions that have a callback as the last argument.
Note that we explicitly use *any* here. This is one of the rare use-cases where *any* makes a lot of sense. We just don't care yet what we're passing as function as long as the shape is intact. We also don't want to be bothered by passing arguments around. This is a helper function, *any* will do fine.
Next, let's work on the return type, a promisified function. The promisified function is a conditional type where we check on the shape we defined in `FunctionWithCallback`. This is where the actual type check happens, and where we bind types to generics.
```typescript
type PromisifiedFunction<T> =
(...args: InferArguments<T>)
=> Promise<InferResults<T>>
```
A `PromisiedFunction<T>` takes all the arguments of the original callback-style function minus the callback. We get this argument list through `InferArguments<T>`.
```typescript
type InferArguments<T> =
T extends (
... t: [...infer R, (...args: any) => any]
) => any ? R : never
```
The conditional checks against the same type declaration as we defined in `FunctionWithCallback`, but instead of letting the arguments before the callback just pass, we infer the whole list of arguments and return them.
The `PromisifiedFunction<T>` returns a Promise with the results that are defined in the callback function. That's why we have to *infer* the results from the original function in the same manner:
```typescript
type InferResults<T> =
T extends (
...t: [...infer T, (res: infer R) => any]
) => any ? R : never
```
The function type we check for in our conditional is again of a very similar shape as `FunctionWithCallback`. This time however we want to know the argument list of the callback and infer those.
This already works like a charm. `loadFile` gets the correct types, and also functions with other types and other argument lists do the same.
```typescript
declare function addAsync(
x: number, y: number,
cb: (result: number) => void
);
const addProm = promisify(addAsync);
// x is number!
addProm(1, 2).then(x => x)
```
The types are done! We already can test how this function will behave once it's finished. And this is all that we need to do on the TypeScript side. A couple of lines of code and we know the input and the output behavior of our function. As this is a utility function, we will most likely use `promisify` more often than once. Time on typings well spent.
The implementation also takes a couple of lines of code:
```typescript
function promisify<
Fun extends FunctionWithCallback
>(f: Fun): PromisifiedFunction<Fun> {
return function(...args: InferArguments<Fun>) {
return new Promise((resolve) => {
function callback(result: InferResults<Fun>) {
resolve(result)
}
args.push(callback);
f.call(null, ...args)
})
}
}
```
Here we can see some behavior mirrored in JavaScript that we already saw in TypeScript. We use rest parameters -- of type InferArguments -- in the newly created function. This returns a Promise with a result of type `InferResults`. To get this, we need to create the callback, let it *resolve* the Promise with the results and add it back to the argument lists. Now e can call the original function with a complete set of arguments.
This is the inverse operation of what we did with our types, where we always tried to shave off the callback function. Here we need to add it again to make it complete.
The types are complex, but so sound that we can implement the whole `promisify` function without any type cast. This is a good sign!
## Lesson 44: JSONify
In this lesson we see:
- Mapped types
- Conditional types
- The *infer* keyword
- Recursive types
Hat tip to Anders Hejlsberg. This is one of the examples he showcased in one of his talks. I spent quite some time scribbling it down from pixelated screen recordings, and even more time figuring out what was happening in just a couple lines of code. After seeing the power and potential of the type system, this was the moment where I became a TypeScript fan.
`JSON.parse` and `JSON.stringify` are nice functions to serialize and deserialize JavaScript objects. JSON is a subset of JavaScript objects, missing only functions and `undefined`. This subset makes parsing JSON strings even faster [than parsing regular objects](https://v8.dev/blog/cost-of-javascript-2019#json).
A possible class that serializes and deserializes JavaScript objects, which can be bound to different types, can look like this:
```typescript
class Serializer<T> {
serialize(inp: T): string {
return JSON.stringify(inp)
}
deserialize(inp: string): JSONified<T> {
return JSON.parse(inp)
}
}
```
With `JSONified` to be defined. Let's declare a type that includes all possible ways of writing values in JavaScript: numbers, strings, booleans, functions. Nested and in arrays. And a type that has a `toJSON` function. If an object has a `toJSON` function, `JSON.stringify` will use the object returned from `toJSON` for serialization, and not the actualy properties.
```typescript
// toJSON returns this object for
// serialization, no matter which other
// properties this type has.
type Widget = {
toJSON(): {
kind: "Widget", date: Date
}
}
type Item = {
// regular primitive types
text: string;
count: number;
// options get preserved
choice: "yes" | "no" | null;
// functions get dropped.
func: () => void;
// nested elements need to be parsed
// as well
nested: {
isSaved: boolean;
data: [1, undefined, 2];
}
// a pointer to another type
widget: Widget;
// the same object referenced again
children?: Item[];
}
```
There is a difference between JSON and JavaScript objects, and we can model this difference with just a few lines of conditional types. Let's implement `JSONified<T>`.
### JSONified values
First, we create the `JSONified` type and do one particular check. Is this an object with a `toJSON` function. If so, we infer the return type and use this. Otherwise we *jsonify* the object itself. `toJSON` also returns an object, so we pass it to our next step as well.
```typescript
type JSONified<T> =
JSONifiedValue<
T extends { toJSON(): infer U } ? U : T
>;
```
Next, let's look at the actual values, and what happens once we serialize them. Primitive types can be transferred easily. Functions should be dropped. Arrays and nested objects should be handled separately.
Let's put this into a type:
```typescript
type JSONifiedValue<T> =
T extends string | number | boolean | null ? T :
T extends Function ? never :
T extends object ? JSONifiedObject<T> :
T extends Array<infer U> ? JSONifiedArray<U> :
never;
```
`JSONifiedObject` is a mapped type where we run through all properties and apply `JSONified` again.
```typescript
type JSONifiedObject<T> = {
[P in keyof T]: JSONified<T[P]>
}
```
This is the first occurence in this book of a recursive type. TypeScript allows for a certain level of recursion as long as there's no circular referencing involved. Calling `JSONified` again down in a tree works.
It's similar with the `JSONifiedArray`. This becomes an array of *jsonified* values. If there's an *undefined* element, `JSON.stringify` will map this to *null*. That's why we need another helper type.
```typescript
type UndefinedAsNull<T> =
T extends undefined ? null : T;
type JSONifiedArray<T> =
Array<UndefinedAsNull<JSONified<T>>>
```
And this is all wee need. With a couple of lines of code, we described the entire behaviour of a `JSON.parse` after a `JSON.stringify`. Not only on a type level, but also wrapped nicely in a class:
```typescript
const itemSerializer = new Serializer<Item>()
const serialization =
itemSerializer.serialize(anItem)
const obj =
itemSerializer.deserializer(serialization)
```
I urge you to try it out in a playground or in your own application. It's remarkable to see how recursive types work and how deep they can go in a nested object structure.
## Lesson 45: Service definitions
In this lesson we see:
- Mapped types
- Conditional types
- String and Number constructors
- Control flow analysis
Another hat tip to Anders Hejlbserg and the TypeScript team. This is the second example that made me love TypeScript and acknowledge the immense powers that lie in the type system.
### Dynamic definitions
We want to provide a helper function for our colleagues where they can define a service and its service methods through a JavaScript object that looks a little like a type. For example, a service definition for opening, closing, and writing to files.
```typescript
const serviceDefinition = {
open: { filename: String },
insert: { pos: Number, text: String },
delete: { pos: Number, len: Number },
close: {},
};
```
The service defnition object describes
- A list of method definitions. Each property of the service definition is a method.
- Each method definition defines a payload and the accompanying type.
We use capital `Number` and capital `String` here, constructor functions in JavaScript that create values of type *number* or *string* respectively. These are not TypeScript types! But they look awfully similar.
The goal is to have a function `createService`, where we pass two things:
1. The service defintion. In the format we described above.
2. A request handler. This is a function that receives messages and payloads, and is implemented by the users of this function. E.g. for the service definition above we expect to get a message `open` with a payload `filename`, where the file name is a string
In return, we get a service object. This service object exposes methods (open, insert, delete, close according to the service definition) that allow for a certain payload (defined in the service definition). Once we call a method, we set up a request object that is handled by the request handler.
### Typing service definitions
As always, before we implement something, let's work on the function head of `createService`. According to the specification above, the plain function definition looks like this:
```typescript
declare function createService<
S extends ServiceDefinition
>(
serviceDef: S,
handler: RequestHandler<S>,
): ServiceObject<S>
```
Note that we only need one generic type variable bound, the service definition. Let's define the service definition types.
```typescript
// a service defnition has keys we don't know
// yet and lots of method definitions
type ServiceDefinition = {
[x: string]: MethodDefinition;
};
// this is the payload of each method
// a key we don't know, and either a string or
// a number constructor (the capital String, Number)
type MethodDefinition = {
[x: string]:
StringConstructor | NumberConstructor;
};
```
This allows for objects with every possible string keys. The moment we bind a concrete service definition to the generic variable, the keys become defined, and we work with the narrowed down type.
Next, we work on the second argument, the request handler. The request handler is a function with one argument, the request object. It returns a boolean if the execution was successful.
```typescript
type RequestHandler<
T extends ServiceDefinition
> = (req: RequestObject<T>) => boolean;
```
### The request object
The request object is defined by the service definition we pass. It's an object where each key of the service definition becomes a message. The object after the key of the service definition becomes the payload.
```typescript
type RequestObject<T extends ServiceDefinition> = {
[P in keyof T]: {
message: P;
payload: RequestPayload<T[P]>;
}
}[keyof T];
```
With the index access type to `keyof T`, we create a union out of an object that would contain every key.
The request payload is defined by the object of each key in the service definition:
```typescript
type RequestPayload<T extends MethodDefinition> =
// is it an empty object?
{} extends T ?
// then we don't have a payload
undefined :
// Otherwise we have the same keys, and get the
// type from the constructor function
{ [P in keyof T]: TypeFromConstructor<T[P]> };
type TypeFromConstructor<T> =
T extends StringConstructor ?
string :
T extends NumberConstructor ? number : any;
```
For the service definition we described earlier, the generated type looks like this:
```typescript
{
req: {
message: "open";
payload: {
filename: string;
};
} | {
message: "insert";
payload: {
pos: number;
text: string;
};
} | {
message: "delete";
payload: {
pos: number;
len: number;
};
} | {
message: "close";
payload: undefined;
}
}
```
### The service object
Last, but not least, we are typing the service object, the return value. For each entry in the service definition, it creates some service methods.
```typescript
type ServiceObject<T extends ServiceDefinition> = {
[P in keyof T]: ServiceMethod<T[P]>
};
```
Each service method takes a payload that is defined in the object after each key in the service definition.
```typescript
type ServiceMethod<T extends MethodDefinition> =
// the empty object?
{} extends T ?
// no arguments!
() => boolean :
// otherwise, its the payload we already
// defined
(payload: RequestPayload<T>) => boolean;
```
That's all the type we need!
### Implementing createService
Now, let's implement the function itself. We create an empty object and add new keys to it. Each key is a method that takes a payload and calls the handler.
```typescript
function createService<S extends ServiceDefinition>(
serviceDef: S,
handler: RequestHandler<S>,
): ServiceObject<S> {
const service: Record<string, Function> = {};
for (const name in serviceDef) {
service[name] =
(payload: any) => handler({
message: name,
payload
});
}
return service as ServiceObject<S>;
}
```
Note that we allow ourselves a little type cast at the end to make sure the very generic creation of a new object is type safe. This is how our service definition looks in action:
```typescript
const service = createService(
serviceDefinition,
req => {
// req is now perfectly typed and we know
// which messages we are able to get
switch (req.message) {
case 'open':
// do somethiing
break;
case 'insert':
// do something
break;
default:
// due to control flow anaysis, this
// message now can only be close or
// delete.
// we can narrow this down until we
// reach never
break;
}
return true;
}
);
// we get full autocomplete for all available
// methods, and know which payload to
// expect
service.open({ filename: 'text.txt' });
// even if we don't have a payload
service.close();
```
We wrote around 25 lines of type definitions and a few implementation details, and our colleagues will be able to write custom services that are 100% type safe. Take any other service definition and play around yourself!
## Lesson 46: DOM JSX Engine, part 1
In this lesson we learn about
- JSX and JSX Factories
There has been hardly a technology more discussed than React in the last couple of years. Well, maybe Typescript! React is a library to compose UI and handle State directly in JavaScript. No other technology needed. Well, except for one: JSX. JSX is a syntax feature that allows you to write HTML or XML like commands directly in JavaScript. Your components look something like this:
```typescript
export function Button() {
return <button>Click me</button>
}
```
If you have never seen something like this, and even if you did, your first reaction might be: "JSX mixes HTML with my JavaScript, that's ugly!" Believe me, that was *my* first reaction.
Rest assured, JSX doesn't mix HTML and JavaScript. Here's what JSX is not:
- JSX is not a templating language
- JSX is not HTML
- JSX is not XML
JSX *looks* like all that, but it's nothing but syntactic sugar.
### JSX is function calls
JSX translates into pure, nested function calls. The React method signature of JSX is `(element, properties, ...children)`. With element being either a React component or a string, properties being a JS object with keys and values. Children being empty, or an array with more function calls.
So:
```typescript
<Button onClick={() => alert('YES')}>Click me</Button>
```
translates to:
```typescript
React.createElement(Button, { onClick: () => alert('YES') }, 'Click me');
```
With nested elements, it looks something like this:
This JSX
```typescript
<Button onClick={() => alert('YES')}><span>Click me</span></Button>
```
translates to:
```typescript
React.createElement(Button, { onClick: () => alert('YES') },
React.createElement('span', {}, 'Click me'));
```
There is one convention: Uppercase elements translate to components. Lowercase elements to strings. The latter is used for standard HTML elements.
What are the implications of that, especially compared to templates?
- There's no runtime compilation and parsing of templates. Everything goes directly to the virtual DOM or layout engine underneath.
- There's no expressions to evaluate. Everything around is JavaScript.
- Every component property is translatable to a JSX object key. This allows us to type check them. TypeScript works so well with JSX because there's JavaScript underneath.
So everything *looks* like XML, except that it's JavaScript functions.
One question to us seasoned web developers: Ever wanted to write to the DOM directly, but gave up because it's so unwieldy? `document.createElement` has an easy enough API, but we have to do a ton of calls to the DOM API to get what weu can achieve so easily by writing HTML.
JSX solves that. With JSX you have a nice and familiar syntax of writing elements without HTML.
### Writing the DOM with JSX
Enter TypeScript! TypeScript is a full blown JSX compiler. With TypeScript, we have the possibility to change the JSX factory. That's how TypeScript is able to compile JSX for React, Vue.js, Dojo... any other framework using JSX in one way or the other. The virtual DOM implementations underneath might differ, but the interface is the same:
```typescript
/**
* element: string or component
* properties: object or null
* ...children: null or calls to the factory
*/
function factory(element, properties, ...children) {
//...
}
```
We can use the same factory method signature not only to work with the virtual DOM, we can also use this to work with the real DOM. Only to have a nice API on top of `document.createElement`.
Let's try! These are the features we want to implement:
1. Parse JSX to DOM nodes, including attributes
2. Have simple, functional components for more composability and flexibility.
Step 1: TypeScript needs to know how to compile JSX for us. Setting two properties in `tsconfig.json` is all we need.
```typescript
{
"compilerOptions": {
...
"jsx": "react",
"jsxFactory": "DOMcreateElement",
"noImplicitAny": false
}
}
```
We leave it to the React JSX pattern (the method signature we were talking earlier), but tell TypeScript to use our soon to be created function `DOMcreateElement` for that.
Also, we set `noImplicitAny` to false for now. This is so we can focus on the implementation, and do proper typings at a later stage. If we want to use JSX, we have to rename our `.ts` files to `.tsx`.
First, we implement our factory function. Its specification
1. Is the element a function, then it's a functional component. We call this function (pass props and children of course) and return the result. We expect a return value of type Node
2. If the element is a string, we parse a regular node
```typescript
function DOMcreateElement(
element, properties, ...children
) {
if(typeof element === 'function') {
return element({
...nonNull(properties, {}),
children
});
}
return DOMparseNode(
element,
properties,
children
);
}
/**
* A helper function that ensures we won't work with null values
*/
function nonNull(val, fallback) { #
return Boolean(val) ? val : fallback
};
```
Next, we parse regular nodes.
1. We create an element and apply all properties from JSX to this DOM node. This means that all properties we can pass are part of `HTMLElement`
2. If available, we append all children.
```typescript
function DOMparseNode(element, properties, children) {
const el = Object.assign(
document.createElement(element),
properties
);
DOMparseChildren(children).forEach(child => {
el.appendChild(child);
});
return el;
}
```
Last, we create the function that handles children. Children can either be:
1. Calls to to the factory function `DOMcreateElement`, returning an `HTMLElement`
2. Text content, returning a `Text`
```typescript
function DOMparseChildren(children) {
return children.map(child => {
if(typeof child === 'string') {
return document.createTextNode(child);
}
return child;
})
}
```
To sum it up:
1. The factory function takes elements. Elements can be of type string or a function.
2. A function element is a component. We call the function, because we expect to get a DOM Node out of it. If the function component has also more function components inside, they will eventually resolve to a DOM Node at some point
3. If the element is a string, we create a regular DOM Node. For that we call `document.createElement`
4. All properties are passed to the newly created Node. Now you might understand why React has something like `className` instead of `class`. This is because the DOM API underneath is also `className`. `onClick` is camel-case, though, which I find a little odd.
5. Our implementation only allows DOM Node properties in our JSX, because of that simple property passing
6. If our component has children (pushed together in an array), we parse children as well and append them.
7. Children can be either a call to `DOMcreateElement`, resolving in a DOM Node eventually. Or a simple string.
8. If it's a string, we create a `Text`. `Text`s can also be appended to a DOM Node.
That's all there is! Look at the following code example:
```typescript
const Button = ({ msg }) => {
return <button onclick={() => alert(msg)}>
<strong>Click me</strong>
</button>
}
const el = (
<div>
<h1 className="what">Hello world</h1>
<p>
Lorem ipsum dolor sit, amet consectetur
adipisicing elit. Quae sed consectetur
placeat veritatis
illo vitae quos aut unde doloribus, minima eveniet et
eius voluptatibus minus aperiam
sequi asperiores, odio ad?
</p>
<Button msg='Yay' />
<Button msg='Nay' />
</div>
)
document.body.appendChild(el);
```
Our JSX implementation returns a DOM Node with all its children. We can even use function components for it. Instead of templates, we work with the DOM directly. But the API is a lot nicer!
So what's missing? Property typings!
## Lesson 46: DOM JSX Engine, part 2
In this lesson we see
- Type maps
- Mapped types
- Conditional types
- Declaration merging
- The JSX namespace
What we created in the previous lesson is already *very* powerful. We can go really far and have a nice API to write to the DOM. Without any library, or any framework! But, for us TypeScript folks, we miss one important thing: Proper typings. Type inference does a lot for us, but with *noImplicitAny* turned off, we miss a lot of important information. Turning it back on, we see red squigglies everywhere. Let's be true to ourselves and create proper typings for our functions. As an added benefit, we also get a really good type inference!
### Typing the factory
Let's start with the three functions that we have. TypeScript at this point really complains mostly about implicit *any*s. So we better throw in some concrete types, like we would have done anyway. For brevity, we just focus on the function heads.
The `nonNull` helper function is easy to type. We take two generics we can bind as we use the function.
```typescript
function nonNull<T, U>(val: T, fallback: U) {
return Boolean(val) ? val : fallback;
}
```
The return type is inferred as `T | U`.
Next, we work on `DOMparseChildren`, as it has the simplest set of arguments. There are just a few types that can be actually children of our DOM tree.
1. `HTMLElement`, the base class of all HTML elements
2. `string`, if we pass a regular string that should be converted into a text node.
3. `Text`, if we created a text node outside that we want to append.
We create a helper union `PossibleChildren` and use this for the argument of `DOMparseChildren`.
```typescript
type PossibleChildren =
HTMLElement | Text | string
function DOMparseChildren(
children: PossibleChildren[]
) {
// ...
}
```
The return type is correctly inferred as `HTMLElement | Text`, as we get rid of `string` and convert them to `Text`.
The next function is `DOMparseNode`, as it has the same signature as `DOMcreateElement`. Let's look at the possible input arguments.
1. `element` can be a string or a function. We want to use a generic to bind the concrete value of an element to a type.
2. `properties` can be either the function arguments of the component function or the set of properties of the respective HTML element.
3. `children` is a set of possible children. We have the type for this already.
To make this properly work, we need a couple of helper types. `Fun` is a much more loose interpretation of `Function`. We need this to infer parameters.
```typescript
type Fun = (...args: any[]) => any;
```
We need to know which HTML element is created when we pass a certain string. TypeScript provides an interface called `HTMLElementTagNameMap`. It's a so-called type map, which means that it is a key-value list of identifiers (tags) and their respective type (subtypes of `HTMLElement`). You can find this list in `lib.dom.ts`.
```typescript
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
// and so on ...
}
```
We want to create a type `CreatedElement`, that returns the element according to the string we pass. If this element doesn't exist, we return `HTMLElement`, the base type.
```typescript
type AllElementsKeys = keyof HTMLElementTagNameMap
type CreatedElement<T> =
T extends AllElementsKeys ? HTMLElementTagNameMap[T] :
HTMLElement
```
We use this helper type to properly define `Props`. If we pass a function, we want to get parameters of this function. If we pass a string, possible properties are a partial of the corresponding HTML element. Without the partial, we would have to define *all* properties. And there are a lot!
```typescript
type Props<T> =
T extends Fun ? Parameters<T>[0] :
T extends string ? Partial<CreatedElement<T>> :
never;
```
Note that we index the first parameter of `Parameters`. This is because JSX function only has one argument, the props. And we need to destructure from a tuple to an actual type.
This is all we need for now. Let's go for `DOMparseNode`. This function only takes care if we pass strings.
```typescript
function DOMparseNode<
T extends string
>(
element: T,
properties: Props<T>,
children: PossibleElements[]
)
```
The function correctly infers properties of `HTMLElement` as return type. Last, but not least, the `DOMcreateElement` function. This one can be a bit tricky as separating between function properties and HTML element properties is not as easy as it looks in the first place. We go best with function overloads as we only have two variants. Also, the `Props` type helps us making a correct connection between the type of `element` and the respective properties.
```typescript
function DOMcreateElement<
T extends string
>(
element: T, properties: Props<T>,
...children: PossibleElements[]
): HTMLElement
function DOMcreateElement<
F extends Fun
>(
element: F, properties: Props<F>,
...children: PossibleElements[]
): HTMLElement
function DOMcreateElement(
element: any, properties: any,
...children: PossibleElements[]
): HTMLElement {
// ...
}
```
We can now use the factory function directly!
### Typing JSX
We still get some type errors when using JSX instead of factory functions. This is because TypeScript has its own JSX parser, and wants to catch JSX problems early on. Right now, TypeScript doesn't know which elements to expect. Therefore, it defaults to *any* for every element.
To change this, we have to extend TypeScript's own JSX namespace and define the return type of created elements. Namespaces are a way in TypeScript to organize code. They were created in a time before ECMAScript modules, and are therefore not used as much anymore. Still, when defining internal interfaces that should be grouped, namespaces are the way to go.
As with interfaces, namespaces allow for declaration merging.
We open the namespaces JSX and define two things:
1. The return element, which is extending the interface *Element*.
2. All available HTML elements, so TypeScript can give us autocompletion in JSX. They are defined in `IntrinsicElements`. We use a mapped type where we copy `HTMLElementTagNameMap` to be a map of Partials. Then we extend `IntrinsicElements` from it. We want to use an interface instead of a type as we want to keep declaration merging open.
```typescript
// Open the namespace
declare namespace JSX {
// Our helper type, a mapped type
type OurIntrinsicElements = {
[P in keyof HTMLElementTagNameMap]:
Partial<HTMLElementTagNameMap[P]>
}
// Keep it open for declaration merging
interface IntrinsicElements
extends OurIntrinsicElements {}
// JSX returns HTML elements. Keep this also
// open for declaration merging
interface Element extends HTMLElement {}
}
```
With that, we get auto-completion for HTML elements and function components. And our little TypeScript-based DOM JSX engine is ready for prime time!
## Lesson 48: Extending Object, part 1
This lesson includes
- Conditional types
- Mapped types
- Ambient declaration files
- Declaration merging
- Custom type predicates
- Constructor interfaces
Hat tip to Mirjam Bรคuerlein, who spent an enormous amount of time to prepare and discuss this example with me.
TypeScript's control flow analysis lets you narrow down from a broader type to a more narrow type:
```typescript
function print(msg: any) {
if(typeof msg === 'string') {
// We know msg is a string
console.log(msg.toUpperCase()) // ๐
} else if (typeof msg === 'number') {
// I know msg is a number
console.log(msg.toFixed(2)) // ๐
}
}
```
This is a type-safety check in JavaScript, and TypeScript benefits from that. However, there are some cases where TypeScript needs a little bit more assistance from us.
### Checking object properties
Let's assume you have a JavaScript object where you don't know if a certain property exists. The object might be `any` or `unknown`. In JavaScript, you would check for properties like that:
```typescript
if(typeof obj === 'object'
&& 'prop' in obj) {
//it's safe to access obj.prop
console.assert(typeof obj.prop !== 'undefined')
// But TS doesn't know :-(
}
if(typeof obj === 'object'
&& obj.hasOwnProperty('prop')) {
//it's safe to access obj.prop
console.assert(typeof obj.prop !== 'undefined')
// But TS doesn't know :-(
}
```
At the moment, TypeScript isn't able to extend the type of `obj` with a `prop`. Even though this works with JavaScript.
We can, however, write a little helper function to get correct typings:
```typescript
function hasOwnProperty<
X extends {}, Y extends PropertyKey
>(
obj: X, prop: Y
): obj is X & Record<Y, unknown> {
return obj.hasOwnProperty(prop)
}
```
Let's check out what's happening:
1. Our `hasOwnProperty` function has two generics:
1. `X extends {}` makes sure we use this method only on objects
2. `Y extends PropertyKey` makes sure that the key is either `string | number | symbol`. `PropertyKey` is a builtin type.
2. There's no need to explicitly define the generics, they're getting inferred by usage.
3. `(obj: X, prop: Y)`: We want to check if `prop` is a property key of `obj`
4. The return type is a type prediacte. If the method returns `true`, we can retype any of our parameters. In this case, we say our `obj` is the original object, with an intersection type of `Record<Y, unknown>`, the last piece adds the newly found property to `obj` and sets it to `unknown`.
In use, `hasOwnProperty` works like that:
```typescript
// person is an object
if(typeof person === 'object'
// person = { } & Record<'name', unknown>
// = { } & { name: 'unknown'}
&& hasOwnProperty(person, 'name')
// yes! name now exists in person ๐
&& typeof person.name === 'string'
) {
// do something with person.name,
// which is a string
}
```
### Extending lib.d.ts
Writing a helper function is a bit on the nose. Why writing a helper function that wraps some baked-in functionality only to get better types? We should be able to create those typings directly where they happen.
Thankfully, with declaration merging of interfaces, we are able to do that. Create your own *ambient* type declaration file and make sure that the TypeScript compiler knows where to find them (`typeRoots` and `types` in `tsconfig.json` are a good start). In this file, which we can call `mylib.d.ts`, we can add our own ambient declarations, and can *extend* existing delcarations.
We can do so with the `Object` interface. This is a built-in interface for all `Object`s.
```typescript
interface Object {
hasOwnProperty<
X extends {},
Y extends PropertyKey
>(this: X, prop: Y): this is X & Record<Y, unknown>
}
```
If you think that TypeScript should have something like that already included in what's shipped, then you are right. There might be some good reasons they don't ship type definitions like that, yet. So it's good that we are able to extend to our own need.
### Extending the object constructor
We get into a similar scenario when working with other parts of *Object*. One pattern that you might come up a lot is to iterate over an array of object keys, then accessing these properties to do something with the values.
```typescript
const obj = {
name: 'Stefan',
age: 38
}
Object.keys(obj).map(key => {
console.log(obj[key])
^^^^^^^^
})
```
In *strict* mode, TypeScript wants to explicitly what type `key` has, to be sure that this index access works. So we get some red squigglies thrown at us.
Well, we *should* know the type of `key` is! It's, well *keyof obj*! This is a good chance to extend the typings for `Object`.
This is how `Object.keys` should behave
1. If we pass a number, we return an empty array.
2. If we pass an array or a string, we get a string array in return. This string array contains the stringified indicies of the input value.
3. If we pass an object, we get the actual keys of this object in return.
The interface to extend is called `ObjectConstructor`. For classes or class-like structures, TypeScript needs two different interfaces. One is the *constructor interface*. It includes the constructor function and all the static information. The other is the *instance interface*. This one includes all the dynamic information per instance.
This divide comes from old JavaScript where classes where defined as constructor function and prototype, for example:
```typescript
// static parts --> constructor interface
function Person(name, age) {
this.name = name
this.age = age;
}
Person.create(name, age) {
return new Person(name, age)
}
// dynamic parts --> instance interface
Person.prototype.toString() {
return `My name is ${this.name} and I'm
${age} years old`
}
```
In our case, `Object` is the instance interface. `ObjectConstructor` is the constructor interface. Let's make `Object.keys` stronger:
```typescript
// A utility type
type ReturnKeys<O> =
O extends number ? [] :
O extends Array<any> | string ? string[] :
O extends object ? Array<keyof O> : never
// extending the interface
interface ObjectConstructor {
keys<O>(obj: O) : ReturnKeys<O>
}
```
Let's put this into our ambient type declaration file and `Object.keys` will get better type inference immediately.
## Lesson 49: Extending Object, part 2
In this lesson, we learn about
- Property descriptors
- Conditional types
- Assertion signatures
- We go full circle
In JavaScript, we can define object properties on the fly with `Object.defineProperty`. This is useful if we want your properties to be read-only or similar. Think back to the very first example of our book. A storage object that has a maximum value that shouldn't be overwritten:
```typescript
const storage = {
currentValue: 0
}
Object.defineProperty(storage, 'maxValue', {
value: 9001,
writable: false
})
console.log(storage.maxValue) // 9001
storage.maxValue = 2
console.log(storage.maxValue) // still 9001
```
`defineProperty` and property descriptors are very complex. They allow us to do everything with properties that usually is reserved for built-in objects. So they're common in larger codebases. TypeScript has a little problem with `defineProperty`:
```typescript
const storage = {
currentValue: 0
}
Object.defineProperty(storage, 'maxValue', {
value: 9001,
writable: false
})
// ๐ฅ Property 'maxValue' does not exist on type...
console.log(storage.maxValue)
```
If we don't explicitly typecast, we don't get `maxValue` attached to the type of `storage`. However, for simple use cases, we can help!
### assertion signatures
With TypeScript 3.7, the team introduced assertion signatures. Think of an `assertIsNum` function where we can make sure some value is of type `number`. Otherwise, it throws an error. This is similar to the `assert` function in Node.js:
```typescript
function assertIsNum(val: any) {
if (typeof val !== "number") {
throw new AssertionError("Not a number!");
}
}
function multiply(x, y) {
assertIsNum(x);
assertIsNum(y);
// at this point I'm sure x and y are numbers
// if one assert condition is not true, this position
// is never reached
return x * y;
}
```
To comply with behavior like this, we can add an assertion signature that tells TypeScript that we know more about the type after this function:
```typescript
function assertIsNum(val: any): asserts val is number {
if (typeof val !== "number") {
throw new AssertionError("Not a number!");
}
}
```
This works a lot like type predicates, but without the control flow of a condition-based structure like `if` or `switch`.
```typescript
function multiply(x, y) {
assertIsNum(x);
assertIsNum(y);
// Now also TypeScript knows that both x and y are numbers
return x * y;
}
```
If we look at it closely, we can see those assertion signatures can **change the type of a parameter or variable on the fly**. This is just what `Object.defineProperty` does as well.
### custom defineProperty
> **Disclaimer**: The following helper does not aim to be 100% accurate or complete. It might have errors, it might not tackle every edge case of the `defineProperty` specification. It might, however, handle a lot of use cases well enough. So use it at your own risk!
Just as with `hasOwnProperty` in the last lesson, we create a helper function that mimics the original function signature:
```typescript
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor>
(obj: Obj, prop: Key, val: PDesc) {
Object.defineProperty(obj, prop, val);
}
```
We work with 3 generics:
1. The object we want to modify, of type `Obj`, which is a subtype of `object`
2. Type `Key`, which is a subtype of `PropertyKey` (built-in), so `string | number | symbol`.
3. `PDesc`, a subtype of `PropertyDescriptor` (built-in). This allows us to define the property with all its features (writability, enumerability, reconfigurability).
We use generics because TypeScript can narrow them down to a very specific unit type. `PropertyKey` for example is all numbers, strings, and symbols. But if we use `Key extends PropertyKey`, we can pinpoint `prop` to be of e.g. type `"maxValue"`. This is helpful if we want to change the original type by adding more properties.
The `Object.defineProperty` function either changes the object or throws an error should something go wrong. Exactly what an assertion function does. Our custom helper `defineProperty` thus does the same.
Let's add an assertion signature. Once `defineProperty` successfully executes, our object has another property. We are creating some helper types for that. The signature first:
```diff
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor>
- (obj: Obj, prop: Key, val: PDesc) {
+ (obj: Obj, prop: Key, val: PDesc):
+ asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.defineProperty(obj, prop, val);
}
```
`obj` then is of type `Obj` (narrowed down through a generic), and our newly defined property.
This is the `DefineProperty` helper type:
```typescript
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends {
writable: any, set(val: any): any
} ? never :
Desc extends {
writable: any, get(): any
} ? never :
Desc extends {
writable: false
} ? Readonly<InferValue<Prop, Desc>> :
Desc extends {
writable: true
} ? InferValue<Prop, Desc> :
Readonly<InferValue<Prop, Desc>>
```
First, we deal with the `writeable` property of a `PropertyDescriptor`. It's a set of conditions to define some edge cases and conditions of how the original property descriptors work:
1. If we set `writable` and any property accessor (get, set), we fail. `never` tells us that an error was thrown.
2. If we set `writable` to `false`, the property is read-only. We defer to the `InferValue` helper type.
3. If we set `writable` to `true`, the property is not read-only. We defer as well
4. The last, default case is the same as `writeable: false`, so `Readonly<InferValue<Prop, Desc>>`. (`Readonly<T>` is built-in)
This is the `InferValue` helper type, dealing with the set `value` property.
```typescript
type InferValue<Prop extends PropertyKey, Desc> =
Desc extends { get(): any, value: any } ? never :
Desc extends { value: infer T } ? Record<Prop, T> :
Desc extends { get(): infer T } ? Record<Prop, T> :
never;
```
Again a set of conditions:
1. Do we have a getter and a value set, `Object.defineProperty` throws an error, so never.
2. If we have set a value, let's infer the type of this value and create an object with our defined property key, and the value type
3. Or we infer the type from the return type of a getter.
4. Anything else: We forgot. TypeScript won't let us work with the object as it's becoming `never`
### Moving it to the object constructor
This already works wonderfully in your code, but if you want to make use of that throughout the whole application, we should put this type declaration in `ObjectConstructor`. Let's move our helpers to `mylib.d.ts` and change the `ObjectConstructor` interface
```typescript
type InferValue<Prop extends PropertyKey, Desc> =
Desc extends { get(): any, value: any } ? never :
Desc extends { value: infer T } ? Record<Prop, T> :
Desc extends { get(): infer T } ? Record<Prop, T> :
never;
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends {
writable: any, set(val: any): any
} ? never :
Desc extends {
writable: any, get(): any
} ? never :
Desc extends {
writable: false
} ? Readonly<InferValue<Prop, Desc>> :
Desc extends {
writable: true
} ? InferValue<Prop, Desc> :
Readonly<InferValue<Prop, Desc>>
interface ObjectConstructor {
defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor
>(obj: Obj, prop: Key, val: PDesc):
asserts obj is Obj & DefineProperty<Key, PDesc>;
}
```
Thanks to declaration merging and function overloading, we attach this much more concrete version of `defineProperty` to `Object`. In use, TypeScript aims for the most correct version when selecting an overload. So we always end up with the one overload where we bind generics through inference. Let's see what TypeScript does:
```typescript
const storage = {
currentValue: 0
}
Object.defineProperty(storage, 'maxValue', {
writable: false, value: 9001
})
storage.maxValue // it's a number
storage.maxValue = 2 // Error! It's read-only
const storageName = 'My Storage'
defineProperty(storage, 'name', {
get() {
return storageName
}
})
storage.name // it's a string!
// it's not possible to assign a value and a getter
Object.defineProperty(storage, 'broken', {
get() {
return storageName
},
value: 4000
})
// storage is never because we have a malicious
// property descriptor
storage
```
We already have some great additions to regular typings that we can re-use in all our applications. Take care of this file and make it part of your standard set-up.
## Epilogue: Lesson 50
Welcome to the last lesson in this book. When we set out on this journey, we had a specific focus. How can we use TypeScript as an extension to JavaScript, the programming language that drives the web? Going this route has led us deep into the realms of type systems, learning how we can model data with union and intersection types, model behavior with generics, and conditional types, trying to reduce type maintenance as much as possible. In the end, we had an arsenal of tools at our hands, making sure that we use the least amount of code possible to define the most complex JavaScript scenarios.
Still, this is just part of what TypeScript has to offer. Arguably, the most important part, but you never know where you end up with your newly found TypeScript skills.
In this lesson, I want to prepare you for the unknown. Not the type *unknown*, but the things that lie ahead. The future, which can't be covered in a book that is supposed to be timeless.
So where do you go from here?
### Listen
I urge you to keep an ear on the ground of the TypeScript team. The TypeScript team works in the open. You can see their team communication on [GitHub](https://github.com/Microsoft/TypeScript), find roadmaps, upcoming features, and their overall plans. We know that even if TypeScript already covers a good chunk of JavaScript scenarios, there are still situations out there where we need a type-cast, or worse, any! This is supposed to change. And the TypeScript team is very vocal about there plans. Especially the [roadmap](https://github.com/microsoft/TypeScript/wiki/Roadmap) is a good read!
You should also keep an ear on the ground of TC39. TC39, the committee that standardizes ECMAScript works closely with the TypeScript team. Some members of the TypeScript team are even part of TC39. New language features are specified by TC39, and implemented first in TypeScript once they reach a certain maturity level and are ready to be implemented in JavaScript engines. Their [GitHub repo](https://github.com/tc39/ecma262) is an excellent source of discussions and new features.
### Learn
We covered a big and important part of TypeScript, but there is more to it. There are features that go deep into object-oriented programming languages. Other language constructs are syntactic sugar that makes certain forms easier to write. And there are experimental features that are required by certain frameworks. The official [TypeScript handbook and documentation](https://typescriptlang.org) are an excellent source to get to the ground of all this.
You will see starter kits for TypeScript with your most favorite framework. YOu also get a good overview of language features that you haven't seen yet and might find useful. As you know by the many interludes in this book, people can be very opinionated about those features. But try them, and make your own opinion!
### Read on
The TypeScript community is very active in providing new tools, writing articles, and showing developers what can be done with the power of this language. There is a new Node.js like runtime by its original creators called [Deno](https://deno.land) out there, which supports TypeScript out of the box. Together with the people from the package manager [Pika](https://www.pika.dev/) they make sure that you get type declaration files over HTTP once you import a Deno package from a URL.
Then there's a myriad of blogs on TypeScript. [Marius Schulz](https://mariusschulz.com/) has been writing for years about TypeScript, and curates a wonderful newsletter called [TypeScript Weekly](https://www.typescript-weekly.com/). Sometimes you find articles in there by me. Which you can read on [my website](https://fettblog.eu).
As the hard rock poets from Deep Purple so masterfully said: Listen, learn, read on! There's a lot to uncover, a lot to learn!
*Thank you for reading my book!*
I hope you had as much joy reading it as I had writing it. As TypeScript is always evolving, I always try to find new and exciting solutions to typing challenges. Reach out to me on Twitter at *@ddprrt*, and let's figure out how we can type your code best!