Introduction to Typed Programming with TypeScript

Learn to write types in a typed language and master TypeScript faster with your existing JavaScript knowledge.

Types themselves are a complex language

I used to think that typescript was just adding type annotations on top of JavaScript. In this mindset, I often find writing the correct types tricky and daunting, to the point that they prevent me from building actual applications in a way, and often lead me to use them any. And with that any, I lose all type safety.

In fact, you can use types to make types very complex. After using typescript for a while, I feel that the typescript language is actually made up of two sub-languages ​​- one is JavaScript and the other is a typed language:

  • For the JavaScript language, the world consists of JavaScript values
  • For typed languages, the world is made up of types

When we write typescript code, we keep dancing back and forth between two worlds: we create types in the type world, and use type annotations (or infer them implicitly by the compiler) to "call" them in our JavaScript world They; we can also go in the other direction: use typescript typeofkeywords on JavaScript variables, properties to retrieve the corresponding type (this is not referring to the typeofkeywords provided by JavaScript to check the type at runtime).

image.png

JavaScript is a very expressive language, and so are typed languages. In fact, expressive type languages ​​have been shown to be Turing-complete.

I'm not making any value judgments here on whether Turing completeness is good or bad, and I don't know if it's even by design or by accident (in fact, many times Turing completeness is achieved by accident). My point is that typed languages ​​themselves, while seemingly innocuous, are certainly powerful, performant, and capable of arbitrary computation at compile time.

When I started to think of the typed language in TypeScript as a full-fledged programming language, I realized that it even had some of the characteristics of functional programming languages.

  • Use recursion instead of iteration
    • In typescript 4.5 we can optimize recursion with tail calls (to some extent)
  • Types (data) are immutable

In this article, we'll learn the type language in typescript by contrasting it with JavaScript, so you can leverage your existing JavaScript knowledge to master typescript faster.

variable declaration

In JavaScript, the world is composed of JavaScript values, and we use the keywords var, let, constto declare variables to refer to the values. For example:

const obj = { name: 'foo' }
复制代码

In a typed language, the world is made up of types, and we use the keywords typeand interfaceto declare type variables. For example:

type Obj = { name: string }
复制代码

Note: For a "type variable", a more accurate name would be a type synonym or a type alias. I use the term "type variable" to analogize how a JavaScript variable refers to a value.

This isn't a perfect analogy though, 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 concepts of typed languages.

Types and values ​​are very related. A type whose core is to represent a collection of possible values, and the valid operations that can be performed on those values. Sometimes this set is finite, eg type Name = 'foo' | 'bar', and more often the set is infinite, eg type Age = number. In TypeScript, we integrate types and values ​​and make them work together to ensure that the value at runtime matches the type at compile time.

local variable declaration

We covered how to create typed variables in a typed language. However, type variables have a global scope by default. To create a local type variable, we can use inferkeywords in the type language.

type A = 'foo'; // global scope
type B = A extends infer C ? (
    C extends 'foo' ? true : false // 只有在这个表达式中,C代表A
) : never;
复制代码

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 letkeyword binding into perform assignment in scope, like let {assignments} in {expression}:

let two = 2; three = 3 in two * three 
//                         ↑       ↑
// two and three are only in scope for the expression `two * three` 
复制代码

Equivalence Comparison and Conditional Branching

在JavaScript中,我们可以使用=====和if语句或者条件(三元)运算符?来执行相等校验和条件分支。

另一方面,在类型语言中,我们使用extends关键字进行“相等检查”,并且条件(三元)运算符?的使用也适用于条件分支:

TypeC = TypeA extends TypeB ? TrueExpression : FalseExpression
复制代码

如果TypeA是可分配给TypeB或者可替代TypeB的,那么我们进入第一个分支,从TrueExpression中获得类型并分配给TypeC;否则我们从FalseExpression中获得类型作为TypeC的结果。

JavaScript中的一个具体例子:

const username = 'foo'
let matched

if (username === 'foo') {
    matched = true
} else {
    matched = false
}
复制代码

将其翻译为类型语言:

type Username = 'foo'
type Matched = Username extends 'foo' ? true : false
复制代码

extends关键字是多功能的。它也可以对通用类型参数应用约束。例如:

function getUserName<T extends {name: string}>(user: T) {
    return user.name
}
复制代码

通过添加通用约束,T extends { name: string },我们确保我们的函数参数总是由字符串类型的name属性组成。

通过对对象类型的索引来检索属性的类型

在JavaScript中,我们可以用方括号来访问对象属性,例如obj['prop']或者点操作符,例如obj.prop

在类型语言中,我们也可以用方括号提取属性类型。

type User = { name: string; age: number; }
type Name = User['name']
复制代码

这不仅适用于对象类型,我们还可以用元组和数组来索引类型。

type Names = string[]
type Name = Names[number]

type Tupple = [string, number]
type Age = Tupple[1]
type Info = Tupple[number]
复制代码

Functions

函数是任何JavaScript程序中主要的可重复使用的“构建块”。它们接收一些输入(some JavaScript values)并返回一个输出(也是some JavaScript values)。在类型语言中,我们有泛型。泛型将类型参数化,就像函数把值参数化一样。因而,泛型在概念上类似于JavaScript中的函数。

比如,在JavaScript中:

function fn (a, b = 'world') {
    return [a, b]
}
const result = fn('hello') // ['hello', 'world']
复制代码

对于类型语言,可以这么做:

type Fn<A extends string, B extends string = 'world'> = [A, B]
//   ↑   ↑           ↑                          ↑          ↑
// name parameter parameter type         default value   function body/return statement

type Result = Fn<'hello'> // ['hello', 'world']
复制代码

但是这仍然不是一个完美的比喻:泛型绝对不是和JavaScript中的函数完全一样。比如有一点,与JavaScript中的函数不同的是,泛型不是类型语言中的一等公民。这意味着我们不能像将函数传给另一个函数那样,将一个泛型传给另一个泛型,因为typescript不允许泛型作为类型参数。

Map和filter

在类型语言中,类型是不可改变的。如果我们想改变一个类型的某个部分,我们必须将现有的类型转成新的类型。在类型语言中,数据结构(即对象类型)遍历细节和均匀地应用转换由映射类型抽象出来。我们可以用它实现概念上类似于JavaScript的数组mapfilter方法。

在JavaScript中,假设我们想把一个对象的属性从数字转换为字符串。

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);
复制代码

在类型语言中,映射是用这种语法[k in keyof T]完成的,其中keyof操作符拿到的是属性名的一个字符串联合类型。

type User = {
    name: string
    age: number
}
type StringifyProp<T> = {
    [K in keyof T]: string
}
type UserWithStringProps = StringifyProp<User> // { name: string; age: string; }
复制代码

在JavaScript中,我们可以基于一些标记来过滤掉一个对象的属性。例如,我们可以过滤掉所有非字符串类型的属性。

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' }
复制代码

在类型语言中,还可以通过as操作符和never类型:

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>;
复制代码

在typescript中,有一堆内置工具类型(泛型)用于转换类型,所以很多时候你不必重复造轮子。

模式匹配

我们还可以用infer关键字在类型语言中进行模式匹配。

例如,在JavaScript应用程序中,我们可以使用正则来提取字符串的某一部分:

const str = 'foo-bar'.replace(/foo-*/, '')
console.log(str) // 'bar'
复制代码

在类型语言中等价于:

type Str = 'foo-bar'
type Bar = Str extends `foo-${infer Rest}` ? Rest : never // 'bar'
复制代码

递归,而不是迭代

就像许多纯函数式编程语言一样,在类型语言中,没有for循环的语法结构来迭代一个数据列表。递归代替了循环的位置。

比方说,在JavaScript中,我们想写一个函数来返回一个数组,其中同一个项重复多次。下面是某种实现方法:

function fillArray(item, n) {
    const res = [];
    for(let i = 0; i < n; i++) {
        res[i] = item;
    }
    return res;
}
复制代码

递归的写法是:

function fillArray(item, n, arr = []) {
    return arr.length === n ? arr : filleArray(item, n, [item, ...arr]);
}
复制代码

我们如何在类型语言中写出这个等价关系?下面是如何得出一个解决方案的逻辑步骤:

  • Create a FillArraygeneric called (remember when we said generics are like functions in typed languages?)
    • FillArray<Item, N extends number, Arr extends Item[] = []>
  • In the "function body" we need to use the extendskeyword to check if Arrthe lengthproperty is alreadyN
    • If it has been met N(the base condition) then we simply returnArr
    • If it hasn't been reached N, it recurses and adds one Itemto Arrit.

Putting this together, 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']
复制代码

upper limit of recursion depth

Before TypeScript 4.5, the maximum recursion depth was 45. In TypeScript 4.5, there are tail call optimizations and the upper limit is increased to 999.

Avoid type gymnastics in production code

Sometimes type programming is jokingly called "type gymnastics" when it gets really complicated, bells and whistles, far more complicated than it needs to be in a typical application. for example:

  • Simulate Chinese Chess
  • Simulated tic-tac-toe game
  • implement arithmetic

These are more of an academic exercise and are not suitable for production applications because:

  • Difficult to understand, especially esoteric typescript features
  • Difficult to debug because compiler error messages are too long and obscure
  • Compilation is slow

Just like having LeetCode to practice your core programming skills, there isType ChallengeCome practice your type programming skills.

concluding remarks

Much has been discussed in this article. The point of this post is not to teach you typescript, but to reintroduce the "hidden" type language you may have overlooked since you started learning typescript.

In the typescript community, type programming is a niche and under-discussed topic, and I don't see anything wrong with that - because in the end adding types is just a means to an end, which is to write a more reliable web in JavaScript application. So to me, it's totally understandable that people don't take the time to "seriously" study typed languages ​​as often as they do with JavaScript or other programming languages.

original

www.zhenghao.io/posts/type-…

Guess you like

Origin juejin.im/post/7079305963131371550