Learn to write types in type languages and learn TypeScript faster with your existing JavaScript knowledge.
Typing is a complex language
I used to think of typescript as just adding type annotations on top of JavaScript. In this mindset, I often find writing the right types so tricky and daunting that they somehow prevent me from building the actual application, and often cause me to use any. By using any, I lose all type safety.
In fact, you can use types and make them very complicated. After working with typescript for a while, I’ve come to the view that the typescript language is actually made up of two sub-languages — one is JavaScript and the other is a type language:
- For the JavaScript language, the world is made up of JavaScript values
- For a typed language, the world is made up of types
When we write typescript code, we constantly dance back and forth between two worlds: we create types in the type world and “conjure” them in our JavaScript world using type annotations (or implicitly inferring them by the compiler); We can also move in the other direction: use typescript typeof keywords on JavaScript variables and properties to retrieve the corresponding types (not the typeof keywords provided by JavaScript to check runtime types).
JavaScript is a very expressive language, and so are typed languages. In fact, expressive type languages have proved to be Turing-complete.
I’m not making any value judgments here about whether Turing completeness is good or bad, or whether it is even by design or by accident (in fact, many times it is achieved by accident). My point is that type languages themselves, while seemingly harmless, are certainly powerful, high-performance, and capable of arbitrary computations at compile time.
When I started thinking of typescript as a full-fledged programming language, I realized that it even had some of the characteristics of a functional programming language.
- Use recursion instead of iteration
- In typescript4.5 we can use tail calls to optimize recursion (to some extent)
- The type (data) is immutable
In this article, we’ll learn about typescript’s type language by comparing it to JavaScript, so you can leverage your JavaScript knowledge to get to grips with typescript faster.
Variable declarations
In JavaScript, the world is made up of JavaScript values, and we use the keywords var, let, and const to declare variables that reference those values. Such as:
const obj = { name: 'foo' }
Copy the code
In a typed language, where the world is made up of types, we use the keywords type and interface to declare type variables. Such as:
type Obj = { name: string }
Copy the code
Note: For type variables, more accurate names are type synonyms or type aliases. I use the term “type variable” as an analogy to how a JavaScript variable refers to a value.
This isn’t a perfect analogy, though, and type aliases don’t create or introduce a new type — they’re just a new name for an existing type. But I hope this analogy makes it easier to explain the concept of type languages.
Types and values are very related. A type that, at its core, represents a collection of possible values and valid operations that can be performed on those values. Sometimes this collection is limited, such as the type Name = ‘foo’ | ‘bar’, more when the set is infinite, such as the type Age = number. In typescript, we integrate types and values and make them work together to ensure that runtime values match compile-time types.
Local variable declaration
We talked about how to create type variables in a type language. However, type variables have a global scope by default. To create a local type variable, we can use the infer keyword in the type language.
type A = 'foo'; // global scope
type B = A extends infer C ? (
C extends 'foo' ? true : false // Only in this expression does C stand for A
) : never;
Copy the code
Although this particular way of creating scoped variables may seem strange to JavaScript developers, it actually finds its roots in some purely functional programming languages. For example, in Haskell, we can use the let keyword with in to perform scoped assignments, such as let {assignments} in {expression} :
let two = 2; three = 3 inTwo * three // two and three are onlyin scope for the expression `two * three`
Copy the code
Equivalence comparison and conditional branching
In JavaScript, we can use the ===, ==, and if statements or the conditional (ternary) operator? To perform the equality checksum conditional branch.
In a type language, on the other hand, we use the extends keyword for “equality checking,” and the condition (ternary) operator? The use of “also applies to conditional branches:
TypeC = TypeA extends TypeB ? TrueExpression : FalseExpression
Copy the code
If TypeA is assignable to TypeB or alternative to TypeB, then we go to the first branch, get the type from TrueExpression and assign it to TypeC; Otherwise we obtain the type from FalseExpression as the result of TypeC.
A concrete example in JavaScript:
const username = 'foo'
let matched
if (username === 'foo') {
matched = true
} else {
matched = false
}
Copy the code
Translate it into type language:
type Username = 'foo'
type Matched = Username extends 'foo' ? true : false
Copy the code
The extends keyword is versatile. It can also apply constraints to generic type parameters. Such as:
function getUserName<T extends {name: string}>(user: T) {
return user.name
}
Copy the code
By adding a generic constraint, T extends {name: String}, we ensure that our function arguments always consist of a name attribute of type string.
The type of an attribute is retrieved by indexing the object type
In JavaScript, we can use square brackets to access object properties, such as obj[‘prop’], or dot operators, such as obj.prop.
In a type language, we can also use square brackets to extract attribute types.
type User = { name: string; age: number; }
type Name = User['name']
Copy the code
This doesn’t just apply to object types; we can also index types with tuples and arrays.
type Names = string[]
type Name = Names[number]
type Tupple = [string.number]
type Age = Tupple[1]
type Info = Tupple[number]
Copy the code
Functions
Functions are the main reusable “building blocks” in any JavaScript program. They take some input (some JavaScript values) and return an output (also some JavaScript values). In type languages, we have generics. Generics parameterize types just like functions parameterize values. Thus, generics are conceptually similar to functions in JavaScript.
For example, in JavaScript:
function fn (a, b = 'world') {
return [a, b]
}
const result = fn('hello') // ['hello', 'world']
Copy the code
For typed languages, you can do this:
type Fn<A extends string, B extends string = 'world'> = [A, B]
// ↑ is up
// name parameter parameter type default value function body/return statement
type Result = Fn<'hello'> // ['hello', 'world']
Copy the code
But it’s still not a perfect metaphor: generics are by no means exactly the same as functions in JavaScript. For example, unlike functions in JavaScript, generics are not first-class citizens of type languages. This means that we can’t pass generics from one function to another the way we pass functions to another, because typescript doesn’t allow generics as type parameters.
The Map and the filter
In a typed language, types are immutable. If we want to change part of a type, we must convert the existing type to the new type. In a type language, data structures (that is, object types) iterate over details and apply transformations uniformly abstracted from mapping types. We can use it to implement the javascript-like array map and Filter methods.
In JavaScript, suppose we want to convert an object’s property from a number to a string.
const user = {
name: 'foo'.age: 28};function stringifyProp (object) {
return Object.fromEntries(
Object.entries(object)
.map(([key, value]) = > [key, String(value)])
)
}
const userWithStringProps = stringifyProp(user);
Copy the code
In type languages, the mapping is done using this syntax [k in keyof T], where the keyof operator takes a string union type of the attribute name.
type User = {
name: string
age: number
}
type StringifyProp<T> = {
[K in keyof T]: string
}
type UserWithStringProps = StringifyProp<User> // { name: string; age: string; }
Copy the code
In JavaScript, we can filter out the attributes of an object based on some tag. For example, we can filter out all attributes that are not strings.
const user = {
name: 'foo'.age: 28};function filterNonStringProp (object) {
return Object.fromEntries(
Object.entires(object)
.filter([key, value] => typeof value === 'string'))}const filteredUser = filterNonStringProp(user) // { name: 'foo' }
Copy the code
In type languages, we can also use the AS operator and the never type:
type User = {
name: string;
age: number;
};
type FilterNonStringProp<T> = {
[K in keyof T as T[K] extends string ? K : never] :string;
};
type FilteredUser = FilterNonStringProp<User>;
Copy the code
In typescript, there are a bunch of built-in utility types (generics) for converting types, so a lot of times you don’t have to duplicate the wheel.
Pattern matching
We can also use the infer keyword to match patterns in type languages.
For example, in a JavaScript application, we can use the re to extract a portion of a string:
const str = 'foo-bar'.replace(/foo-*/.' ')
console.log(str) // 'bar'
Copy the code
In type languages it is equivalent to:
type Str = 'foo-bar'
type Bar = Str extends `foo-${infer Rest}` ? Rest : never // 'bar'
Copy the code
Recursion, not iteration
As with many purely functional programming languages, there is no syntactic structure for a for loop to iterate over a list of data in a typed language. Recursion takes the place of the loop.
For example, in JavaScript, we want to write a function that returns an array with the same item repeated multiple times. Here’s a way to do it:
function fillArray(item, n) {
const res = [];
for(let i = 0; i < n; i++) {
res[i] = item;
}
return res;
}
Copy the code
The recursion is written as:
function fillArray(item, n, arr = []) {
return arr.length === n ? arr : filleArray(item, n, [item, ...arr]);
}
Copy the code
How do we write this equivalence in a type language? Here are the logical steps to arrive at a solution:
- Create a file called
FillArray
(Remember we talked about generics being like functions in a type language?)FillArray<Item, N extends number, Arr extends Item[] = []>
- In the “function body”, we need to use
extends
Keyword checkArr
thelength
Whether the property is alreadyN
- If it’s already there
N
(Base condition), then we simply returnArr
- If it hasn’t already
N
, it will recurse and add oneItem
toArr
In the.
- If it’s already there
Put these together and we have:
type FillArray<Item, N extends number, Arr extends Item[] = []> =
Arr['length'] extends N ? Arr : FillArray<Item, N, [...Arr, Item]>;
type Foos = FillArray<'foo'.3> // ['foo', 'foo', 'foo']
Copy the code
The upper limit of recursion depth
Prior to typescript4.5, the maximum recursion depth was 45. In typescript4.5, there is tail-call optimization and the upper limit is increased to 999.
Avoid type gymnastics in production code
Type programming is sometimes derisively referred to as “type gymnastics,” when it becomes very complex and frilly, far more complex than is needed in a typical application. For example:
- Simulated Chinese Chess
- Simulated tic-Tac-toe
- To realize the arithmetic
These are more like academic exercises and not suitable for production applications because:
- Hard to understand, especially the esoteric typescript features
- Debugging is difficult because the compiler error messages are too long and obscure
- Slow compilation
Just as there is LeetCode to practice your core programming skills, there are type challenges to practice your type programming skills.
conclusion
Much has been covered in this article. The point of this article is not to teach you typescript, but to reintroduce you to the “hidden” type languages that you might have overlooked since you started learning typescript.
Type programming is a small and underdiscussed topic in the typescript community, and I don’t see anything wrong with that — ultimately adding types is just a means to an end, which is writing more reliable Web applications in JavaScript. So it makes perfect sense to me that people don’t spend as much time “seriously” studying type languages as they do with JavaScript or other programming languages.
The original
www.zhenghao.io/posts/type-…