在TypesScript中思考的思维模型

This article was first published at TK's blog.

本文最初在TK的博客上发表

One day I came across a tweet from Lari Mazza that says

有一天,我遇到了来自Lari Mazza的一条推文 ,内容是:

“Can I make a suggestion? Types are hard to understand when you’ve only worked with JS in your life and suddenly have to learn TypeScript"

“我可以提出建议吗? 当您一生仅使用JS并突然不得不学习TypeScript时,很难理解类型。”

As a software engineer that learned Python, Ruby, Javascript, and Clojure first, when I tried C++, it was a horror movie. I couldn’t do much, so counterproductive, and frustrating. Maybe because I was doing everything wrong and I didn’t understand types the right way.

作为一名软件工程师,当我尝试C ++时,他首先学习了Python,Ruby,Javascript和Clojure,这是一部恐怖电影。 我不能做太多,所以适得其反,令人沮丧。 可能是因为我做错了所有事情,而且我不正确地理解类型。

But even though I had so many problems, I could implement a bunch of algorithms and data structures.

但是即使我有很多问题,我也可以实现一堆算法和数据结构

Now I’m using more and more Typescript in my day-to-today job and my side projects, I feel I’m more prepared to confront types. Actually, not confront, but use them in my favor.

现在,我在日常工作和辅助项目中使用了越来越多的Typescript,我觉得我已经准备好面对类型了。 实际上,不是对抗,而是使用它们对我有利。

This post is my attempt to help developers think more in types and understand this mental model.

这篇文章是我试图帮助开发人员更多地思考类型并理解这种心理模型的尝试。

JavaScript类型 (JavaScript types)

If you’re here, you probably heard that Typescript is a superset of Javascript. If not, great, you just learned something new today. YAY!

如果您在这里,您可能会听说Typescript是Javascript的超集。 如果不是,那就太好了,您今天才学到了一些新东西。 好极了!

Typescript is a superset because any Javascript code is valid in Typescript, syntactically speaking. It may or may not compile depending on the Typescript compiler configuration. But in terms of syntax, it works just fine. This is why you can migrate Javascript to Typescript progressively by just replacing the .js extension with the .ts. Everything will be without type declarations (the any type), but that's another story.

从语法上讲,Typescript是一个超集,因为任何Javascript代码在Typescript中都是有效的。 根据Typescript编译器的配置,它可能会编译也可能不会编译。 但是就语法而言,它可以正常工作。 这就是为什么您只需将.js扩展替换为.ts即可逐步将Javascript迁移到Typescript的原因。 一切都将没有类型声明( any类型),但这是另一回事。

Also, if you code in Javascript — or any other programming language — you probably think in types:

另外,如果您使用Javascript或任何其他编程语言进行编码,则可能会考虑以下类型:

  • “Hm, it is a list of integers, so I’ll need to filter only the even numbers and return a new list”

    “嗯,它是一个整数列表,所以我只需要过滤偶数并返回一个新列表”
  • “This is an object, but I just need to get this string value from the property X”

    “这是一个对象,但我只需要从属性X获取此字符串值”
  • “This function receives two parameters. Both A and B are integers and I want to sum them”

    “此功能接收两个参数。 A和B都是整数,我想对它们求和”

Yeah, you got the idea. We think in types. But they are just in our heads. We constantly think about them because we need to know how to handle, parse, or modify data. We need to know which methods we are allowed to use in this object type.

是的,您知道了。 我们考虑类型。 但是它们只是在我们的脑海中。 我们一直在思考它们,因为我们需要知道如何处理,解析或修改数据。 我们需要知道允许在此对象类型中使用哪些方法。

To give a more concrete example, imagine you want to sum the price of all products. A product object looks like this:

举一个更具体的例子,假设您想对所有产品的价格求和。 产品对象如下所示:

const product = {
title: 'Some product',
price: 100.00,
};

But now with a list of products:

但是现在有了产品列表:

const products = [
{
title: 'Product 1',
price: 100.00,
},
{
title: 'Product 2',
price: 25.00,
},
{
title: 'Product 3',
price: 300.00,
}
];

Ok! Now we want a function to sum all the products prices.

好! 现在,我们需要一个函数来汇总所有产品的价格。

function sumAllPrices(products) {
return products.reduce((sum, product) => sum + product.price, 0);
};sumAllPrices(products); // 425

Just receive the products as the argument and reduce all product prices. Javascript works just fine. But while building this function you start to think about the data and how to handle it properly.

只是接受产品作为参数并降低所有产品价格。 JavaScript可以正常工作。 但是,在构建此功能时,您开始考虑数据以及如何正确处理它。

The first part: products as an argument. Here you just think: “well, we’re receiving a list of some objects”. Yeah, in our heads the products are a list. This is why we can think of using the reduce method. It is a method from the Array prototype.

第一部分:以产品为论点。 您在这里只是在想:“好吧,我们正在接收一些对象的列表”。 是的,在我们的头脑中,产品列表。 这就是为什么我们可以考虑使用reduce方法的原因。 它是Array原型的一种方法。

Then we can think about the object in detail. We know that the product object has a price property. And this property is a number. This is why we can do product.price and sum with the accumulator.

然后,我们可以详细考虑该对象。 我们知道产品对象具有price属性。 这个属性是一个数字。 这就是为什么我们可以用累加器执行product.price和sum的原因。

Recapping:

重新封底:

  • products is a list of objects.

    products是对象列表。

  • As a list, we can use the reduce method, as this method a member of the Array prototype.

    作为列表,我们可以使用reduce方法,因为该方法是Array原型的成员。

  • The produce object has some properties. One of them is the price, which is a number.

    produce对象具有一些属性。 其中之一是price ,它是一个数字。

  • As a number property, we can use it to sum with the reduce accumulator.

    作为数字属性,我们可以使用它与reduce累加器求和。
  • We wanted to return a number, the sum of all product prices.

    我们想返回一个数字,即所有产品价格的总和。

We are always thinking of data types, we just need to add the type annotations to make it more explicit and ask the compiler for help. Our memory is limited and the compilers are here to help us, humans.

我们一直在考虑数据类型,我们只需要添加类型注释以使其更加明确,然后向编译器寻求帮助。 我们的记忆力有限,编译器在这里为人类提供帮助。

The type system will not only make our data more consistent, but it can also provide autocompletion for data types. It knows the types, so it can show the members for the data. We will take a look at this idea later. Here I just wanted to show that we think in types in our heads.

类型系统不仅使我们的数据更加一致,而且还可以为数据类型提供自动补全功能。 它知道类型,因此可以显示数据的成员。 稍后我们将研究这个想法。 在这里,我只是想表明我们头脑中的思维类型。

简单类型和简单用途 (Simples Types & Simple Uses)

So we are ready to use some strongly typed programming languages like Typescript. We simply need to explicitly add type annotations to our data structures. It’s simple. But sometimes it’s not that easy (usually it’s not easy when you come from dynamically typed languages. You feel unproductive. It feels like a battle against types. The idea here is to make this learning curve more smooth and fun).

因此,我们准备使用一些强类型编程语言,例如Typescript。 我们只需要在数据结构中显式添加类型注释即可。 这很简单。 但是有时并不是那么容易(通常,来自动态类型语言时,这并不容易。您会感觉效率低下。这就像在与类型作斗争。这里的想法是使学习曲线更流畅,更有趣)。

Here we will see many examples of how to use types in Typescript. We start with easy and silly examples and progressively make it more complex while designing the mental model to think in types.

在这里,我们将看到许多有关如何在Typescript中使用类型的示例。 我们从简单而愚蠢的示例开始,逐步将其变得更复杂,同时设计可进行类型思考的思维模型。

As Javascript, Typescript also has basic data types like number, string, boolean, null, etc. You can find all the basic data types in the Typescript Docs.

作为Javascript,Typescript还具有基本数据类型,例如numberstringbooleannull等。您可以在Typescript Docs中找到所有基本数据类型。

With these units of data, we can make our programs more useful. To be more practical, let’s get a simple example. A sum function.

使用这些数据单位,我们可以使我们的程序更有用。 为了更实际,让我们举一个简单的例子。 sum函数。

How does it work in Javascript?

它如何在Javascript中工作?

function sum(a, b) {
return a + b;
}

Is everything ok? ok.

一切都顺利吗? 好。

Now let’s use it:

现在让我们使用它:

sum(1, 2); // 3
sum(2, 2); // 4
sum(0, 'string'); // '0string' WTF!

The first two calls are what we expect to happen in our system. But Javascript is very flexible, it lets us provide any value to this function. The last call is bizarre. We can call with a string, but it will return an unexpected result. It doesn’t break in development, but it will result in strange behavior in runtime.

前两个调用是我们期望在系统中发生的调用。 但是Javascript非常灵活,它可以让我们为该函数提供任何价值。 最后一个电话很奇怪。 我们可以用字符串调用,但是它将返回意外的结果。 它不会破坏开发,但会在运行时导致奇怪的行为。

What do we want? We want to add some constraints to the function. It will only be able to receive numbers. That way, we narrow the possibility to have unexpected behaviors. And the function return type is also a number.

我们想要什么? 我们要向函数添加一些约束。 它只能接收号码。 这样,我们缩小了发生意外行为的可能性。 并且函数返回类型也是一个数字。

function sum(a: number, b: number): number {
return a + b;
}

Great! It was very simple. Let’s call again.

大! 这很简单。 让我们再次打电话。

sum(1, 2); // 3
sum(2, 2); // 4
sum(0, 'string'); // Argument of type '"string"' is not assignable to parameter of type 'number'.

As we type annotate our function, we provide information to the compiler to see if everything is correct. It will follow the constraints we added to the function.

当我们键入注释函数时,我们会向编译器提供信息,以查看一切是否正确。 它将遵循我们添加到函数中的约束。

So the first two calls are the same as in Javascript. It will return the correct calculation. But the last one we have an error in compile time. This is important. The error now happens in compile time and prevents us to ship incorrect code to production. It says that the string type is not part of the set of values in the number type universe.

因此,前两个调用与Javascript中的相同。 它将返回正确的计算。 但是最后一个我们在编译时出错。 这个很重要。 该错误现在发生在编译时,并阻止我们将不正确的代码发送到生产环境。 它说string类型不是number类型Universe中的值集的一部分。

For basic types, we just need to add a colon followed by the type definition.

对于基本类型,我们只需要在类型定义后面添加一个冒号即可。

const isTypescript: boolean = true;
const age: number = 24;
const username: string = 'tk';

Now let’s increase the challenge. Remember the product object code we wrote in Javascript? Let’s implement it again, but now with the Typescript mindset.

现在让我们增加挑战。 还记得我们用Java编写的产品目标代码吗? 让我们再次实现它,但现在要使用Typescript思维方式。

Just to remember what we are talking about:

只记得我们在说什么:

const product = {
title: 'Some product',
price: 100.00,
};

This is the product value. It has a title as string and the price as number. For now, this is what we need to know.

这是产品价值。 它的titlestringpricenumber 。 目前,这是我们需要知道的。

The object type would be something like:

对象类型如下:

{ title: string, price: number }

And we use this type to annotate our function:

我们使用这种类型来注释我们的功能:

const product: { title: string, price: number } = {
title: 'Some product',
price: 100.00,
};

With this type, the compiler will know how to handle inconsistent data:

使用这种类型,编译器将知道如何处理不一致的数据:

const wrongProduct: { title: string, price: number } = {
title: 100.00, // Type 'number' is not assignable to type 'string'.
price: 'Some product', // Type 'string' is not assignable to type 'number'.
};

Here it breaks in two different properties:

在这里,它分为两个不同的属性:

  • The title is a string and should not receive a number.

    title是一个string ,不应接收number

  • The price is a number and should not receive a string.

    price是一个number ,不应包含string

The compiler helps us to catch type errors like that.

编译器帮助我们捕获类似的类型错误。

We could improve this type annotation by using a concept called Type Aliases. It's a way to create a new name for a specific type.

我们可以通过使用名为Type Aliases的概念来改进此类型注释。 这是一种为特定类型创建新名称的方法。

In our case, the product type could be:

在我们的情况下,产品类型可以是:

type Product = {
title: string;
price: number;
};const product: Product = {
title: 'Some product',
price: 100.00,
};

It’s better to visualize the type, add semantics, and maybe reuse in our system.

最好在我们的系统中可视化类型,添加语义并可能重用。

Now that we have this product type, we can use it to type the products list. The syntax looks like this: MyType[]. In our case, Product[].

现在我们有了这种产品类型,我们可以使用它来键入产品列表。 语法如下所示: MyType[] 。 在我们的例子中, Product[]

const products: Product[] = [
{
title: 'Product 1',
price: 100.00,
},
{
title: 'Product 2',
price: 25.00,
},
{
title: 'Product 3',
price: 300.00,
}
];

Now the function sumAllPrices. It will receive the product and return a number, the sum of all product prices.

现在,函数sumAllPrices 。 它将收到产品并返回一个数字,即所有产品价格的总和。

function sumAllPrices(products: Product[]): number {
return products.reduce((sum, product) => sum + product.price, 0);
};

This is very interesting. As we typed the product, when we write product., it will show the possible properties we can use. In the product type case, it will show the properties price and title.

这很有趣。 当我们键入产品时,即我们编写product. ,它将显示我们可以使用的可能属性。 在产品类型的情况下,它将显示属性pricetitle

sumAllPrices(products); // 425
sumAllPrices([]); // 0
sumAllPrices([{ title: 'Test', willFail: true }]); // Type '{ title: string; willFail: true; }' is not assignable to type 'Product'.

Passing the products will result in the value 425. An empty list will result in the value 0. And if we pass an object with a different structure - Typescript has a structural type system and we will dig deep into this topic later - the compiler will throw a type error telling that the structure is not part of the Product type.

通过products将得到值425 。 空列表将导致值0 。 而且,如果我们传递的对象具有不同的结构-Typescript具有结构类型系统,我们稍后将深入探讨该主题-编译器将引发类型错误,表明该结构不属于Product类型。

结构性打字 (Structural Typing)

Structural typing is a type of type compatibility. It’s a way to understand the compatibility between types based on its structure: features, members, properties. Some languages have type compatibility based on the names of the types, and it’s called nominal typing.

结构化类型是类型兼容性的一种。 这是一种根据类型的结构了解类型之间兼容性的方法:功能,成员,属性。 某些语言具有基于类型名称的类型兼容性,这称为标称类型。

For example, in Java, even if different types have the same structure, it will throw a compile error because we are using a different type to instantiate and define a new instance.

例如,在Java中,即使不同的类型具有相同的结构,它也会引发编译错误,因为我们使用了不同的类型来实例化和定义新实例。

class Person {
String name;
}

class Client {
String name;
}

Client c = new Person(); // compiler throws an error
Client c = new Client(); // OK!

In nominal type systems, the relevant part of a type is the name, not the structure.

在名义类型系统中,类型的相关部分是名称,而不是结构。

Typescript, on another hand, verifies the structural compatibility to allow or not specific data. Its type system is based on structural typing.

另一方面,Typescript验证结构兼容性以允许或不允许使用特定数据。 其类型系统基于结构类型。

The same code implementation that crashes in Java, would work in Typescript.

在Java中崩溃的相同代码实现也可以在Typescript中使用。

class Person {
name: string;
}class Client {
name: string;
}const c1: Client = new Person(); // OK!
const c2: Client = new Client(); // OK!

We want to use the Client type, and it has the property name, to point to the Person type. It also has the property type. So Typescript will understand that both types have the same shape.

我们要使用Client类型,它具有属性name ,以指向Person类型。 它还具有属性类型。 因此,Typescript将理解这两种类型具有相同的形状。

But it is not only about classes, but it works for any other “object”.

但这不仅与类有关,而且还适用于任何其他“对象”。

const c3: Client = {
name: 'TK'
};

This code compiles too because we have the same structure here. The typescript type system doesn’t care about if it is a class, or an object literal if it has the same members, it will be flexible and compile.

该代码也可以编译,因为此处具有相同的结构。 打字稿类型系统不关心它是一个类,还是对象常量如果它具有相同的成员,它将是灵活的和可编译的。

But now we will add a third type: the Customer.

但是现在我们将添加第三种类型: Customer

class Customer {
name: string;
age: number;
};

It not only has the name property, but also the age. What would happen if we instantiate a Client instance in a constant of type Customer?

它不仅具有name属性,而且具有age 。 如果我们以类型Customer的常量实例化Client实例,将会发生什么?

const c4: Customer = new Client();

The compiler will not accept that. We want to use the Customer, that has name and age. But we are instantiating the Client that has only the name property. So it doesn't have the same shape. It will cause an error:

编译器不会接受。 我们要使用具有nameageCustomer 。 但是,我们将实例化仅具有name属性的Client 。 因此它没有相同的形状。 它将导致错误:

Property 'age' is missing in type 'Client' but required in type 'Customer'.

The other way around would work because we want Client, and Customer has all the properties (name) from Client.

周围的其他方法将工作,因为我们希望ClientCustomer拥有的所有属性( name来自) Client

const c5: Client = new Customer();

It works fine!

工作正常!

We can go on for enums, object literals, and any other type, but the idea here is to understand that the structure of the type is the relevant part.

我们可以继续使用枚举,对象文字和任何其他类型,但是这里的想法是要理解类型的结构是相关的部分。

运行时和编译时间 (Runtime and Compile time)

This is a much more complex topic in programming language theory, but I wanted to give some examples to distinct runtime and compile time.

这是编程语言理论中一个非常复杂的话题,但是我想举一些例子说明不同的运行时和编译时间。

Basically, the runtime is the execution time of a program. Imagine your backend receiving data from a frontend form page, handling this data, and saving it. Or when your frontend is requesting data from a server to render a list of Pokemons products.

基本上,运行时间是程序的执行时间。 想象一下,您的后端从前端表单页面接收数据,处理该数据并保存。 或者,当您的前端从服务器请求数据以呈现Pokemons产品列表时。

Compile time is basically when the compiler is executing operations in the source code to satisfy the programming language requirements. It can include type checking as an operation for example. Compile time errors in Typescript, for example, is very related to the code that we wrote before:

编译时间基本上是指编译器在源代码中执行操作以满足编程语言要求时的时间。 例如,它可以包括类型检查作为操作。 例如,Typescript中的编译时错误与我们之前编写的代码非常相关:

  • When the type is missing property: Property 'age' is missing in type 'Client' but required in type 'Customer'.

    当缺少类型的属性时: Property 'age' is missing in type 'Client' but required in type 'Customer'.类型中缺少属性Property 'age' is missing in type 'Client' but required in type 'Customer'.

  • When the type doesn’t match: Type '{ title: string; willFail: true; }' is not assignable to type 'Product'.

    当类型不匹配时:输入Type '{ title: string; willFail: true; }' is not assignable to type 'Product'. Type '{ title: string; willFail: true; }' is not assignable to type 'Product'.

Let’s see some examples to have a better understanding.

让我们看一些示例以更好地理解。

I want to write a function to get the index of a part of the passed programming language.

我想编写一个函数来获取所传递的编程语言一部分的索引。

function getIndexOf(language, part) {
return language.indexOf(part);
}

It receives the language and the part that we will look for to get the index.

它接受languagepart ,我们将寻找到的景气指数。

getIndexOf('Typescript', 'script'); // 4
getIndexOf(42, 'script'); // Uncaught TypeError: language.indexOf is not a function at getIndexOf

When passing a string, it works fine. But passing a number, we got a runtime error Uncaught TypeError. Because a number doesn't have an indexOf function, so we can't really use it.

传递字符串时,它可以正常工作。 但是传递一个数字,我们得到了运行时错误Uncaught TypeError 。 因为数字没有indexOf函数,所以我们不能真正使用它。

But if we give type information to the compiler, in compile time, it will throw an error before running the code.

但是,如果我们将类型信息提供给编译器,则在编译时,它将在运行代码之前引发错误。

function getIndexOf(language: string, part: string): number {
return language.indexOf(part);
}

Now our program knows that it will need to receive two strings and return a number. The compiler can use this information to throw errors when we get a type error… before runtime.

现在我们的程序知道它将需要接收两个字符串并返回一个数字。 当我们在运行时之前遇到类型错误时,编译器可以使用此信息引发错误。

getIndexOf('Typescript', 'script'); // 4
getIndexOf(42, 'script'); // Argument of type '42' is not assignable to parameter of type 'string'.

Maybe, for small projects (or small functions like ours) we don’t really see too much benefit. In this case, we know that we need to pass a string, so we won’t pass a number to the function. But when the codebase grows or you have many people adding code and more complexity, it’s clear to me that a type system can help us a lot to get errors in compile time before shipping code to production.

也许,对于小型项目(或像我们这样的小型职能),我们并没有看到太多好处。 在这种情况下,我们知道我们需要传递一个字符串,因此我们不会将数字传递给该函数。 但是,随着代码库的增长,或者您增加了代码的复杂性,或者增加了很多人的复杂性,对我来说很明显,类型系统可以帮助我们在将代码交付生产之前在编译时获得很多错误。

At first, we need all the learning curve to understand types and all the mental models, but after a while, you’ll be more used to type annotations and eventually become friends with the compiler. It would be a helper, not a yeller.

首先,我们需要所有学习曲线来理解类型和所有心理模型,但是过了一会儿,您将习惯于键入注释并最终与编译器成为朋友。 这将是一个帮手 ,而不是一个小贩

As we are learning about the basic difference between compile time and runtime, I think it’s great to differentiate types from values.

当我们了解编译时间和运行时之间的基本区别时,我认为将类型与值区分开是一件很棒的事情。

All the examples I’ll show here can be copied and run in the Typescript Playground to understand the compiler and the result of the compilation process (aka the “Javascript”).

我将在此处显示的所有示例都可以复制并在Typescript Playground中运行,以了解编译器和编译过程的结果(也称为“ Javascript” )。

In Typescript, we have two different universes: the value and the type spaces. The type space is where types are defined and used to enable the compiler to do all the great magic. And the value space is the values in our programs like variables, constants, functions, value literals, and things that we have in runtime.

在Typescript中,我们有两个不同的Universe:值空间和类型空间。 类型空间是定义类型的地方,可用于使编译器完成所有不可思议的任务。 值空间是程序中的值,例如变量,常量,函数,值文字以及我们在运行时拥有的东西。

It’s good to have an understanding of this concept because in Typescript we can’t use type checking in runtime. It has a very clear separation between type checking and the compilation process.

理解这个概念非常好,因为在Typescript中我们不能在运行时使用类型检查。 它在类型检查和编译过程之间有一个非常清晰的区分。

Typescript has the process of type checking the source code types and sees if everything is correct and consistent. And then it can compile to Javascript. As these two parts are separate, we can’t use type checking in runtime. Only in “compile time”. If you try to use a type as a value, it will throw an error: only refers to a type, but is being used as a value here.

Typescript具有对源代码类型进行类型检查的过程,并查看所有内容是否正确且一致。 然后可以将其编译为Javascript。 由于这两部分是分开的,因此我们不能在运行时使用类型检查。 仅在“编译时”。 如果尝试将类型用作值,则将引发错误: only refers to a type, but is being used as a value here

Let’s see examples of this idea.

让我们来看这个想法的例子。

Imagine we want to write a function called purchase where we receive a payment method and based on this method, we want to do some action. We have a credit card and a debit card. Let's define them here:

假设我们要编写一个名为purchase的函数,在该函数中我们接收一种付款方式,并基于该方式执行一些操作。 我们有信用卡和借记卡。 让我们在这里定义它们:

type CreditCard = {
number: number;
cardholder: string;
expirationDate: Date;
secutiryCode: number;
};type DebitCard = {
number: number;
cardholder: string;
expirationDate: Date;
secutiryCode: number;
};type PaymentMethod = CreditCard | DebitCard;

These types are in the Type space, so it only works in compile time. After type checking this function, the compiler removes all the types.

这些类型在Type空间中 ,因此仅在编译时有效。 在对该函数进行类型检查之后,编译器将删除所有类型。

If you add these types in the Typescript Playground, the output will be only a strict definition "use strict";.

如果在Typescript Playground中添加这些类型,则输出将只是严格定义"use strict";

The idea here is to really understand that the types live in the Type space and will not be available in the runtime. So in our function, it won’t be possible to do this:

这里的想法是要真正理解类型存在于类型空间中,并且在运行时将不可用。 因此,在我们的函数中,将无法执行以下操作:

const purchase = (paymentMethod: PaymentMethod) => {
if (paymentMethod instanceof CreditCard) {
// purchase with credit card
} else {
// purchase with debit card
}
}

In compiler throws an error: 'CreditCard' only refers to a type, but is being used as a value here..

在编译器中引发错误: 'CreditCard' only refers to a type, but is being used as a value here.

The compiler knows the difference between the two spaces and that the type CreditCard lives in the Type space.

编译器知道两个空间之间的区别,并且类型CreditCard类型空间中

The playground is a very cool tool to see the output of your Typescript code. If you create a new credit card object like this:

游乐场是一个非常酷的工具,可以查看您的Typescript代码的输出。 如果您像这样创建一个新的信用卡对象:

const creditCard: CreditCard = {
number: 2093,
cardholder: 'TK',
expirationDate: new Date(),
secutiryCode: 101
};

The compiler will type check it and do all the magic and then it transpiles the Typescript code to Javascript. And we have this:

编译器将对其进行类型检查并进行所有处理,然后将Typescript代码转换为Javascript。 我们有这个:

const creditCard = {
number: 2093,
cardholder: 'TK',
expirationDate: new Date(),
secutiryCode: 101
};

The same object, but now only with the value and without the type.

相同的对象,但现在仅具有值而没有类型。

约束和类型缩小 (Constraints & Type Narrowing)

When we restrict what we can do, it’s easier to understand what we can do.

当我们限制我们可以做什么时,更容易理解我们可以做什么。

We use types as constraints to limit the bugs in your program. To understand this concept, I’m stealing an example from Lauren Tan’s talk about Type Systems.

我们使用类型作为约束来限制程序中的错误。 为了理解这个概念,我从Lauren Tan关于Type Systems的讨论中窃取了一个例子。

const half = x => x / 2;

How many ways does this function can fail? Imagine a number of possible inputs:

该功能有多少种方法会失败? 想象许多可能的输入:

[
null,
undefined,
0,
'0',
'TK',
{ username: 'tk' },
[42, 3.14],
(a, b) => a + b,
]

And what are the results for input:

输入的结果是什么:

half(null); // 0
half(undefined); // NaN
half(0); // 0
half('0'); // 0
half('TK'); // NaN
half({ username: 'tk' }); // NaN
half([42, 3.14]); // NaN
half((a, b) => a + b); // NaN

We have different and unexpected results here. Here it’s clear that we want a number as the half function, do the calculation, and great, it's done! But sometimes we don't control the input or the codebase is big, or new/unfamiliar, and we're able to make these little mistakes.

我们在这里有不同而出乎意料的结果。 显然,我们希望将数字用作half函数,进行计算,好极了! 但是有时我们无法控制输入,或者代码库很大,或者是新的/陌生的,并且我们能够犯这些小错误。

The idea of adding constraints to our code is to narrow the possibilities of a range of types. In this case, we want to limit the input type to a number type. It's the only type that we care about to do the half calculation. With type narrowing, we again give type information to the compiler.

在我们的代码中添加约束的想法是缩小一系列类型的可能性。 在这种情况下,我们希望将输入类型限制为number类型。 这是我们唯一关心的一半计算类型。 通过缩小类型,我们再次将类型信息提供给编译器。

const half = (x: number) => x / 2;

And with this new information, if we call the function with the test cases again, we have different results:

有了这些新信息,如果我们再次用测试用例调用该函数,我们将得到不同的结果:

half(null); // Argument of type 'null' is not assignable to parameter of type 'number'.
half(undefined); // Argument of type 'undefined' is not assignable to parameter of type 'number'.(
half(0); // 0
half('0'); // Argument of type '"0"' is not assignable to parameter of type 'number'.
half('TK'); // Argument of type '"TK"' is not assignable to parameter of type 'number'.
half({ username: 'tk' }); // Argument of type '{ username: string; }' is not assignable to parameter of type 'number'.
half([42, 3.14]); // Argument of type 'number[]' is not assignable to parameter of type 'number'.
half((a, b) => a + b); // Argument of type '(a: any, b: any) => any' is not assignable to parameter of type 'number'.

Basically the compiler will tell us that only the number type, in this case, the 0 value, is a valid input, it will compile, and allow to run the code. We narrow the input type and allow only the value we really want for this function.

基本上,编译器会告诉我们只有数字类型(在这种情况下为0值)是有效输入,它将进行编译并允许运行代码。 我们缩小输入类型并只允许我们对该函数真正想要的值。

But are other ways to narrow the types in Typescript. Imagine we have a function that receives a parameter that can be either a string or a number.

但是还有其他方法可以缩小Typescript中的类型。 假设我们有一个函数可以接收一个可以是字符串或数字的参数。

type StringOrNumber = string | number;function stringOrNumber(value: StringOrNumber) {}

In the function body, the compiler won’t know which methods or properties we can use for this type. Is it a string or number? We only know about the value in runtime. But we can narrow the type using the typeof:

在函数体中,编译器不知道我们可以为该类型使用哪些方法或属性。 是字符串还是数字? 我们只知道运行时的值。 但是我们可以使用typeof来缩小类型:

function stringOrNumber(value: StringOrNumber) {
if (typeof value === 'string') {
// value.
// your ide will show you the possible methods from the string type
// (parameter) value: string
value
} if (typeof value === 'number') {
// value.
// your ide will show you the possible methods from the number type
// (parameter) value: number
value
}
}

With an if statement and the typeof, we can give more information to the compiler. Now it will know the specific type for each if body.

使用if语句和typeof ,我们可以向编译器提供更多信息。 现在它将知道每个if主体的具体类型。

The IDE knows what to show for the specific type. In runtime, when the value is a string, it will go to the first if statement, and the compiler will infer that the type is a string: (parameter) value: string.

IDE知道要显示特定类型的内容。 在运行时,当值是字符串时,它将转到第一个if语句,并且编译器将推断类型为字符串:( (parameter) value: string

When the value is a number, it will go to the second if statement and the compiler will infer that a type is a number: (parameter) value: number.

当值是数字时,它将转到第二个if语句,并且编译器将推断类型为数字:( (parameter) value: number

The if statement can be a helper to the compiler.

if语句可以为编译器提供帮助。

Another example is when we have an optional property in an object, but in a function, we need to return a value based on this optional value.

另一个示例是,当我们在对象中具有可选属性,但在函数中,我们需要基于此可选值返回一个值。

Imagine we have this type:

假设我们有这种类型:

type User = {
name: string;
address: {
street: string;
complement?: string;
}
};

It’s a simple User type. Let's focus on the complement property. It's optional (take a closer look at the ? symbol), which means that it can be a string or undefined.

这是一种简单的User类型。 让我们专注于complement属性。 它是可选的(仔细阅读?符号),这意味着它可以是stringundefined

Now we want to build a function to receive the user and get the length of the address complement. What about this?

现在,我们要构建一个函数来接收用户并获取地址补码的长度。 那这个呢?

function getComplementLength(user: User): number {
return user.address.complement.length;
// (property) complement?: string | undefined
// Object is possibly 'undefined'.
}

As we see earlier, the complement can be a string or undefined. undefined doesn't really have a property called length:

如前所述, complement可以是stringundefinedundefined确实没有一个叫做length的属性:

Uncaught TypeError: Cannot read property 'length' of undefined

We could make something like:

我们可以做类似的事情:

function getComplementLength(user: User) {
return user.address.complement?.length;
}

If the complement has a string value, we can call length, otherwise, it will return undefined. So this function has two possible return types: number | undefined. But we want to ensure that we only return number. So we use a if or a ternary condition to narrow the type. It will only call .length when it has real value (or when it is not undefined).

如果complement具有字符串值,我们可以调用length ,否则,它将返回undefined 。 因此,此函数有两种可能的返回类型: number | undefined number | undefined 。 但是我们要确保只返回number 。 因此,我们使用if或三元条件来缩小类型。 仅当具有实际值(或undefined )时,它才会调用.length

function getComplementLength(user: User): number {
return user.address.complement
? user.address.complement.length
: 0;
}

If it is undefined, we return the minimum length: 0. Now we can use the function with the right type design with and without the complement. Without compile and runtime errors.

如果undefined ,则返回最小长度: 0 。 现在,我们可以在带有和不带有补码的情况下将函数与正确的类型设计一起使用。 没有编译和运行时错误。

getComplementLength({
name: 'TK',
address: {
street: 'Shinjuku Avenue'
}
}); // 0getComplementLength({
name: 'TK',
address: {
street: 'Shinjuku Avenue',
complement: 'A complement'
}
}); // 12

We’ll get 0 from the first function call and 12 from the second call.

我们将从第一个函数调用中获取0 ,并从第二个调用中获取12

With this if concept, we can also use other helpers to do the same thing. We could use the in operator to verify a property from an object, a Array.isArray to verify an array, or the instanceof for any other class type.

有了这个if概念,我们还可以使用其他助手来做同样的事情。 我们可以使用in运算符来验证对象的属性,可以使用Array.isArray来验证数组,或者使用任何其他类类型的instanceof

We could also use more advanced concepts like assertion function or type guards, but I’ll let these concepts to future posts.

我们还可以使用更高级的概念,例如断言函数或类型防护,但我将在以后的文章中介绍这些概念。

One thing that I want to dig deep into this Constraints topic is immutability.

我想深入研究此约束主题的一件事是不变性。

In Javascript and Typescript, we have the idea of mutable objects. If you define value in a variable, we can reassign it with another value later.

在Javascript和Typescript中,我们有了可变对象的概念。 如果您在变量中定义值,我们稍后可以将其重新分配给另一个值。

let email = 'harry.potter@mail.com';
email // 'harry.potter@mail.com'
email = 'hermione.granger@mail.com';
email // 'hermione.granger@mail.com'

Now imagine you have a list of numbers. And you want to use a function to sum all of its numbers. The function looks like this:

现在,假设您有一个数字列表。 您想使用一个函数将所有数字求和。 该函数如下所示:

function sumNumbers(numbers: number[]) {
let sum = 0;
let num = numbers.pop(); while (num !== undefined) {
sum += num;
num = numbers.pop();
} return sum;
}

You call the function passing your list and get the result. It works just fine.

您调用传递您的列表的函数并获得结果。 它工作正常。

const list = [1, 2, 3, 4];
sumNumbers(list); // 10

But what happened to your list? Did the function mutate it entirely?

但是您的名单出了什么事? 该功能是否完全将其变异?

list; // []

If we use the list, it’s empty now. The pop in the sumNumbers function is a "mutate" function. It gets the references and removes the item from them. It's not a copy, it's the real reference.

如果我们使用列表,则该列表为空。 sumNumbers函数中的pop是“变异”函数。 它获取参考并从参考中删除项目。 它不是副本,而是真正的参考。

In runtime, we can use other functions or ways to do the same thing: using reduce, do a for loop without the need to pop items from the array.

在运行时中,我们可以使用其他函数或方式来执行相同的操作:使用reduce,执行for循环,而无需从数组中pop项目。

But using Typescript, we can provide immutability in compile time. If you are not using types, it’s possible to use a type assertion as const. Imagine this:

但是使用Typescript,我们可以在编译时提供不变性。 如果不使用类型,则可以将类型断言as const 。 想象一下:

const author = {
name: 'Walter Isaacson',
email: 'walter.isaacson@mail.com',
books: [
{
title: 'Leonardo Da Vinci',
price: 50.00,
}
]
};author.books.push({
title: 'Steve Jobs',
price: 10.00
});

Just an author object and then we add a new book to this author. The push method updates the book's array reference. It's a "mutate" method. Let's see if you use the const assertion as const:

只是一个作者对象,然后我们向该作者添加一本新书。 push方法更新书的数组引用。 这是一种“变异”方法。 让我们看看是否将const断言as const

const author = {
name: 'Walter Isaacson',
email: 'walter.isaacson@mail.com',
books: [
{
title: 'Leonardo Da Vinci',
price: 50.00,
}
]
} as const;author.books.push({
title: 'Steve Jobs',
price: 10.00
});
// Property 'push' does not exist on type
// 'readonly [{ readonly title: "Leonardo Da Vinci"; readonly price: 50; }]'

The compiler won’t compile. It gets an error on the author’s object. It’s is now readonly, and as a readonly object, it has no method called push (or any "mutate" method). We added a constraint to the author's object. Before it was a specific type (with all the "mutate" methods), and now we narrowed the type to be almost the same, but without the "mutate" methods. Type narrowing.

编译器将无法编译。 它在作者的对象上出错。 现在它是只读的,作为只读对象,它没有称为push方法(或任何“ mutate”方法)。 我们向作者的对象添加了约束。 在使用特定类型(具有所有“突变”方法)之前,现在我们将类型的范围缩小到几乎相同,但没有“变异”方法。 类型缩小。

To continue, let’s add types to this object. The book and the author:

要继续,让我们向该对象添加类型。 bookauthor

type Book = {
title: string;
price: number;
};type Author = {
name: string;
email: string;
books: Book[];
};

Add the type to the author object:

将类型添加到作者对象:

const author: Author = {
name: 'Walter Isaacson',
email: 'walter.isaacson@mail.com',
books: [
{
title: 'Leonardo Da Vinci',
price: 50.00,
}
]
};

Add the type to a new book object:

将类型添加到新书对象:

const book: Book = {
title: 'Steve Jobs',
price: 30
};

And now we can add the new book to the author:

现在我们可以将新书添加到作者中:

author.name = 'TK';
author.books.push(book);

It works just fine!

它很好用!

I want to show another way to add immutability in compile time. Typescript has an utility type called Readonly.

我想展示另一种在编译时增加不变性的方法。 Typescript的实用程序类型为Readonly

You can add the readonly for each property in an object. Something like this:

您可以为对象中的每个属性添加readonly 。 像这样:

type Book = {
readonly title: string;
readonly price: number;
};

But it can be very repetitive. So we can use the Readonly utility to add the readonly to all properties of an object:

但这可能是非常重复的。 因此,我们可以使用Readonly实用程序将readonly添加到对象的所有属性中:

type Book = Readonly<{
title: string;
price: number;
}>;

One thing to keep in mind is that it doesn’t add the readonly for nested properties. For example, if we add the Readonly to the Author type, it won't add the readonly to the Book type too.

要记住的一件事是,它不会为嵌套属性添加只读。 例如,如果将“ Readonly添加到“ Author类型,则也不会将“ readonly添加到“ Book类型。

type Author = Readonly<{
name: string;
email: string;
books: Book[];
}>;

All the properties from the author can’t be reassigned, but you can mutate the books list here (push, pop, ...) because the Book[] is not readonly. Let's see it.

无法重新分配作者的所有属性,但是您可以在此处更改books列表( pushpop ,...),因为Book[]不是只读的。 让我们来看看它。

const author: Author = {
name: 'Walter Isaacson',
email: 'walter.isaacson@mail.com',
books: [
{
title: 'Leonardo Da Vinci',
price: 50.00,
}
]
};const book: Book = {
title: 'Steve Jobs',
price: 30
};author.books.push(book);
author.books;
/* =>
*
* [
* {
* title: 'Leonardo Da Vinci',
* price: 50.00,
* },
* {
* title: 'Steve Jobs',
* price: 30
* }
* ]
*
*/

The push will work just fine.

push将正常工作。

So, how do we enforce a readonly to the books? We need to make sure that the array is a readonly type. We can use the Readonly, or use another utility from Typescript called ReadonlyArray. Let's see the two ways to do it.

那么,我们如何对books强制执行只读操作? 我们需要确保该数组是只读类型。 我们可以使用Readonly ,也可以使用Typescript中的另一个实用程序ReadonlyArray 。 让我们看看两种方法。

With Readonly:

使用Readonly

type Author = Readonly<{
name: string;
email: string;
books: Readonly<Book[]>;
}>;

With ReadonlyArray:

随着ReadonlyArray

type Author = Readonly<{
name: string;
email: string;
books: ReadonlyArray<Book>;
}>;

For me, both work great! But in my opinion, ReadonlyArray is more semantic and I also feel it is less verbose (not that the Readonly with an array is).

对我来说,两者都很棒! 但是在我看来, ReadonlyArray更具语义,而且我也觉得它不太冗长(不是带有数组的Readonly )。

What happened if we try to mutate the author object now?

如果我们现在尝试更改作者对象,会发生什么?

author.name = 'TK'; // Cannot assign to 'name' because it is a read-only property.
author.books.push(book); // Property 'push' does not exist on type 'readonly [{ readonly title: "Leonardo Da Vinci"; readonly price: 50; }]'.

Great! Now we can catch mutable operations in compile time. This is a way to use the concept of adding constraints to our types to make sure they only do what is really needed.

大! 现在我们可以在编译时捕获可变操作。 这是使用为类型添加约束的概念的方法,以确保它们仅满足实际需要。

语义和可读性 (Semantics & Readability)

At first, I felt that Typescript could be very verbose because of the types and make the code much more complex than it should be. And it actually can. Strive for simplicity is the goal and it is difficult at the same time.

刚开始,我觉得Typescript可能由于类型而非常冗长,并使代码比应有的复杂得多。 它实际上可以。 追求简单是目标,但同时又很困难。

This idea is very related to clean code and how we can write code to be human-readable and maintainable. Typescript is no different. Most of the cases, we don’t need super complex types. Let the simple types do the work.

这个想法与干净的代码以及我们如何编写易于理解和维护的代码有关。 打字稿也没什么不同。 大多数情况下,我们不需要超复杂类型。 让简单类型完成工作。

Another thing that I find very useful is semantic of types.

我发现非常有用的另一件事是类型的语义。

Imagine you need to add a string to the sessionStorage to save it in the browser. Your function looks like this:

假设您需要向sessionStorage添加一个字符串以将其保存在浏览器中。 您的函数如下所示:

function saveMyString(value: string): any {
sessionStorage.myString = value;
}

You add a type annotation to the string input and as you don’t know about the returning type, you probably add a any type.

您将类型注释添加到字符串输入中,并且您不知道返回的类型,因此可能添加了any类型。

But what’s the real meaning behind this returning type? Is it returning anything?

但是,这种返回类型背后的真正含义是什么? 返回什么了吗?

It just saves the string to the sessionStorage. It doesn't return anything. The void type was what you're looking for. As Typescript docs says: the absence of having any type at all.

它只是将字符串保存到sessionStorage 。 它不返回任何东西。 void类型是您要寻找的。 如Typescript的文档所述:根本the absence of having any type at all

function saveMyString(value: string): void {
sessionStorage.myString = value;
}

Great, the meaning of the type is correct now. The correctness is very important in a type system. It’s a way to model our data, but also help maintain systems for future developers. Even if the developer is … you!

太好了,该类型的含义现在是正确的。 正确性在类型系统中非常重要。 这是对数据建模的一种方法,但也有助于为将来的开发人员维护系统。 即使开发人员是…您!

Before we were talking about verbose code. And we can improve a lot of our code by using Typescript type inference.

在我们讨论冗长的代码之前。 通过使用Typescript类型推断,我们可以改进很多代码。

For some code, we don’t need to explicitly add type annotation. The Typescript compiler will understand and infer it implicitly. For example:

对于某些代码,我们不需要显式添加类型注释。 Typescript编译器将隐式理解和推断它。 例如:

const num: number = 1;

This code is redundant. We can just let the compiler infers it like this:

此代码是多余的。 我们可以让编译器这样推断:

const num = 1;

In our example earlier, we add the annotation void to the saveMyString function. But as the function doesn't return any value, the compiler will infer that the returning type is void implicitly.

在前面的示例中,我们将注释void添加到saveMyString函数。 但是由于函数不返回任何值,所以编译器将推断返回的类型隐式为void

When I learned this, I thought with myself. But one of the biggest advantages of using Typescript (or any other type system / static type language) is types as documentation. If we let the compiler infer most of the types, we won’t have the documentation we want.

当我学到这一点时,我就想了一下。 但是使用Typescript(或任何其他类型系统/静态类型语言)的最大优势之一是将类型用作文档。 如果让编译器推断大多数类型,我们将没有所需的文档。

But if you hover over the Typescript code in your editor (at least VS Code works like that), you can see the type information and relevant documentation.

但是,如果将鼠标悬停在编辑器中的Typescript代码上(至少VS Code这样工作),则可以看到类型信息和相关文档。

Let’s see other examples of redundant code and make the code less verbose and let the compiler works for us.

让我们看一下冗余代码的其他示例,使代码不那么冗长,并使编译器为我们工作。

function sum(a: number, b: number): number {
return a + b;
};

We don’t need the returning type number, because the compiler knows that a number + another number is equal to a number type, and it is the returning type. It can be:

我们不需要返回类型number ,因为编译器知道一个number +另一个number等于一个number类型,它就是返回类型。 有可能:

function sum(a: number, b: number) {
return a + b;
};

Implicit code, but with documentation, and the compiler does the work.

隐式代码,但带有文档,由编译器来完成。

Type inference works for methods too:

类型推断也适用于方法:

function squareAll(numbers: number[]): number[] {
return numbers.map(number => number * number);
};

This function gets a list of numbers and makes every number a squared value. The returning type is number[], even though the result of a map is always a list, and as we have a list of numbers, it will always be a list of numbers. So we let the compiler infers this too:

此函数获取数字列表,并使每个数字成为平方值。 返回类型为number[] ,即使映射的结果始终是列表,并且由于我们拥有数字列表,所以它也始终是数字列表。 因此,我们也让编译器进行推断:

function squareAll(numbers: number[]) {
return numbers.map(number => number * number);
};

This works the same way for objects too.

这对于对象也是如此。

const person: { name: string, age: number } = {
name: 'TK',
age: 24
};

A person object with a string name and a number age. But as we are assigning these values, the compiler can infer these types.

具有字符串名称和数字年龄的人员对象。 但是,当我们分配这些值时,编译器可以推断出这些类型。

const person = {
name: 'TK',
age: 24
};

If you hover the person, you get this:

如果将鼠标悬停在此person ,则会得到以下信息:

const person: {
name: string;
age: number;
}

The types are documented here.

这些类型在此处记录。

Another benefit of type inference is that we can easily refactor our code. It’s a simple example, but good to illustrate the refactoring process. Let’s get the sum function again.

类型推断的另一个好处是我们可以轻松地重构代码。 这是一个简单的示例,但是很好地说明了重构过程。 让我们再次获得sum函数。

function sum(a: number, b: number): number {
return a + b;
};

Instead of returning the sum number, we want to return "Sum: {a + b}". So for a = 1 and b = 2, we have the resulting string as "Sum: 3".

而不是返回总和,我们要返回"Sum: {a + b}" 。 因此,对于a = 1b = 2 ,我们得到的字符串为"Sum: 3"

function sum(a: number, b: number): string {
return `Sum: ${a + b}`;
};sum(1, 2); // Sum: 3

Great! But now letting the compiler infers this.

大! 但是现在让编译器进行推断。

// function sum(a: number, b: number): number
function sum(a: number, b: number) {
return a + b;
};// function sum(a: number, b: number): string
function sum(a: number, b: number) {
return `Sum: ${a + b}`;
};

We just need to modify the returning value and the type inference will work. No need to think about the returning type. This is a small example, but for more complex functions, it would work too.

我们只需要修改返回值,类型推断就可以了。 无需考虑返回类型。 这是一个小例子,但是对于更复杂的功能,它也可以工作。

Back to the readability part, we can use Enum. A utility that defines a set of named constants. It's a way to give more meaning to the data in your application.

回到可读性部分,我们可以使用Enum 。 定义一组命名常量的实用程序。 这是为应用程序中的数据赋予更多含义的一种方式。

In your node app or a frontend app, you possibly do some fetching to request data. You commonly use a fetch object to perform a request and sometimes you need to pass the accept headers.

在您的节点应用程序或前端应用程序中,您可能需要进行一些提取以请求数据。 通常,您使用提取对象执行请求,有时您需要传递accept标头。

fetch('/pokemons', {
headers: {
Accept: 'application/json'
}
});fetch('/harry-potter/spells', {
headers: {
Accept: 'application/json'
}
});

It’s good, but we can also use an enum to separate this accept string in a constant and reuse.

很好,但是我们也可以使用一个枚举来将这个接受字符串分隔为一个常量并重用。

enum MediaTypes {
JSON = 'application/json'
}fetch('/pokemons', {
headers: {
Accept: MediaTypes.JSON
}
});fetch('/harry-potter/spells', {
headers: {
Accept: MediaTypes.JSON
}
});

And we are able to add more data related to the MediaTypes like PDF:

而且我们能够添加与MediaTypes相关的更多数据,例如PDF

enum MediaTypes {
JSON = 'application/json',
PDF = 'application/pdf'
}

With Enum, we can encapsulate data into a meaningful block of code.

使用Enum ,我们可以将数据封装到有意义的代码块中。

Recently, I was implementing a “state” React component. It’s basically a component that renders an empty state or an error state based on the request response.

最近,我正在实现一个“状态” React组件。 它基本上是一个根据请求响应呈现空状态或错误状态的组件。

The UI for the empty and the error states were very similar. Only the title and the description text and the image icon were different. So I thought: “I have two ways in my mind to implement this: do the logic outside the component and pass all the information needed or pass a ‘state type’ and let the component render the correct icon and messages.”

UI的空白和错误状态非常相似。 仅标题和描述文本以及图像图标不同。 因此,我想:“我有两种方法可以实现此目的:在组件外部进行逻辑处理并传递所需的所有信息,或者传递“状态类型”,然后让组件呈现正确的图标和消息。”

So I built an enum:

所以我建立了一个枚举:

export enum StateTypes {
Empty = 'Empty',
Error = 'Error'
};

And I could just pass this data to the component as the type:

我可以将此数据作为type传递给组件:

import ComponentState, { StateTypes } from './ComponentState';<ComponentState type={StateTypes.Empty} />
<ComponentState type={StateTypes.Error} />

In the component, it had a state object with all the information related to the title, description, and icon.

在组件中,它具有一个状态对象,其中包含与titledescriptionicon有关的所有信息。

const stateInfo = {
Empty: {
title: messages.emptyTitle,
description: messages.emptyDescription,
icon: EmptyIcon,
},
Error: {
title: messages.errorTitle,
description: messages.errorDescription,
icon: ErrorIcon,
},
};

So I could just receive the type based on the enum and use this stateInfo object with the State component from our design system:

因此,我可以只接收基于枚举的类型,并将此stateInfo对象与我们设计系统中的State组件一起使用:

export const ComponentState = ({ type }) => (
<State
title={stateInfo[type].title}
subtitle={stateInfo[type].subtitle}
icon={stateInfo[type].icon}
/>
);

This is a way to use an enum to encapsulate important data into a meaningful block of code in your application.

这是一种使用枚举将重要数据封装到应用程序中有意义的代码块中的方法。

Another cool feature from Typescript is optional properties. When we have properties from an object that can be a real value or undefined, we use an optional property to be explicitly that the property can be or not be there. The syntax for this is a simple ? operator in the object property. Imagine this function:

Typescript的另一个很酷的功能是可选属性。 当我们从某个对象获得的属性可以是真实值或未定义的属性时,我们将使用一个可选属性来明确表明该属性可以存在或不存在。 语法很简单? 对象属性中的运算符。 想象一下这个函数:

function sumAll(a: number, b: number, c: number) {
return a + b + c;
}

But now the c value is optional:

但是现在c值是可选的:

function sumAll(a: number, b: number, c?: number) {
return a + b + c;
}

We add the ? after c. But now we have a compiler error saying:

我们添加?c之后。 但是现在我们有一个编译器错误,说:

(parameter) c: number | undefined
Object is possibly 'undefined'.

We can’t sum an undefined value (well, actually in Javascript we can, but we receive a NaN value).

我们无法求和undefined值(嗯,实际上在Java语言中可以,但是我们收到的是NaN值)。

We need to ensure that the c exists. Type narrowing!

我们需要确保c存在。 类型变窄!

function sumAll(a: number, b: number, c?: number) {
if (c) {
return a + b + c;
} return a + b;
}

If the c exists, it will be a number and we can sum all. If not, sum only the a and b values.

如果c存在,它将是一个number ,我们可以将所有number相加。 如果不是,则仅求和ab值。

An interesting part of this optional property is that it is a undefined not null. This is why we do this, we get a compile error:

此可选属性有趣的部分是它是undefined而不是null 。 这就是我们这样做的原因,我们得到一个编译错误:

let number = null;
sumAll(1, 2, number);
// Argument of type 'null' is not assignable to parameter of type 'number | undefined'.

As the ? operator doesn't handle the null value, choose to use the undefined type in your application and so you can still use the optional property and make the types consistent. We can use it like this:

作为? 运算符不处理null值,而是选择在应用程序中使用undefined类型,因此您仍然可以使用optional属性并使这些类型保持一致。 我们可以这样使用它:

let value: number | undefined;
sumAll(1, 2, value); // 3

If you add a default value to the parameter, you won’t need the ? operator. Actually, the compiler will say that the Parameter cannot have question mark and initializer.

如果您向参数添加默认值,则不需要? 操作员。 实际上,编译器会说Parameter cannot have question mark and initializer

function sumAll(a: number, b: number, c: number = 3) {
return a + b + c;
}

Optional properties not only works on variables and parameters, but also in objects.

可选属性不仅适用于变量和参数,还适用于对象。

An API response is a good example of type definition and optional property together. In API responses, data can be optional. Sometimes the API sends, sometimes it has no value.

API响应很好地说明了类型定义和可选属性。 在API响应中,数据可以是可选的。 有时API发送,有时没有价值。

How we model our types is really important for an application. If an optional property is defined as a required type, we can make our application breaks in runtime. But if we design the types correctly, we have the possible errors in compile time.

我们如何为类型建模对应用程序而言确实很重要。 如果将可选属性定义为必需类型,则可以使应用程序在运行时中断。 但是,如果我们正确地设计类型,则在编译时可能会出现错误。

Imagine we are fetching a user data and this is the way we modeled the response type:

假设我们正在获取用户数据,这就是我们对响应类型进行建模的方式:

type UserResponse = {
name: string;
email: string;
username: string;
age: number;
isActive: boolean;
};

But in reality, the email is optional for the user. The API endpoint could return or not. But the UserResponse type we built treat it as a required property.

但实际上,电子邮件对于用户是可选的。 API端点可以返回还是不返回。 但是我们构建的UserResponse类型将其视为必需属性。

After fetching the user data, we want to see if the user email matches with a specific domain.

提取用户数据后,我们要查看用户电子邮件是否与特定域匹配。

function matchDomain(email: string) {
return email.endsWith(domain);
}

As the email property is required in the UserResponse type, the email parameter will also be required in the matchDomain function.

由于UserResponse类型中的email属性是必需的,所以matchDomain函数中也将需要email参数。

This is the runtime we can get if the email is undefined:

如果email undefined这是我们可以获得的运行时:

// Uncaught TypeError: Cannot read property 'endsWith' of undefined

But what would happen if we modeled the UserResponse correctly?

但是,如果我们正确地对UserResponse建模,会发生什么?

type UserResponse = {
name: string;
email?: string;
username: string;
age: number;
isActive: boolean;
};

Now the email is possibly undefined and it is explicit.

现在, email可能undefined且是明确的。

But if we still keep the function matchDomain the same way, we get a compile error:

但是,如果我们仍然以相同的方式保持功能matchDomain出现编译错误:

// Argument of type 'undefined' is not assignable to parameter of type 'string'.

And this is great! Now we can fix the email parameter in this function using the ? operator:

这太好了! 现在我们可以使用?修复此功能中的email参数? 操作员:

function matchDomain(email?: string) {
return email.endsWith('email.com');
}

But now we get a compile error when running email.endsWith, because it could be undefined too:

但是现在我们在运行email.endsWith时遇到了一个编译错误,因为它也可能是undefined

// (parameter) email: string | undefined
// Object is possibly 'undefined'.

Type narrowing! We use an if block to return a false when the email is undefined. And run endsWith method only if the email is really a string:

类型变窄! 当email undefined时,我们使用if块返回false 。 并且仅在email确实是字符串的情况下运行endsWith方法:

function matchDomain(email?: string) {
if (!email) return false;
return email.endsWith('email.com');
}

It’s pretty nice when we can get runtime errors in compile time. Better to code than debugging after we ship in production, isn’t it?

当我们在编译时遇到运行时错误时,这是​​非常好的。 投产后比调试好代码,不是吗?

类型组成 (Type composition)

Type composition is very useful when trying to reuse existing types for new places of the codebase. We don’t need to rewrite new types, we can create a new type by composing existing ones.

当尝试将现有类型重用于代码库的新位置时,类型组合非常有用。 我们不需要重写新的类型,我们可以通过组合现有的类型来创建新的类型。

One example of composition I always have to handle using Redux or the useReducer hook from React is the idea of "reducers". A reducer can always receive a number of different actions.

我一直必须使用Redux或React的useReducer钩子来处理合成的一个例子是“ reducers”的想法。 减速器总是可以收到许多不同的动作。

In this context, actions are objects with at least a type property. It looks like this:

在这种情况下,动作是至少具有type属性的对象。 看起来像这样:

enum ActionTypes {
FETCH = 'FETCH'
}type FetchAction = {
type: typeof ActionTypes.FETCH;
};const fetchAction: FetchAction = {
type: ActionTypes.FETCH
};

A fetchAction has a type FetchAction that has a property type that is a typeof FETCH.

一个fetchAction的类型为FetchAction ,其属性类型为type FetchAction FETCH

But a reducer can receive other actions too. For example a submit action:

但是减速器也可以接受其他动作。 例如提交动作:

enum ActionTypes {
FETCH = 'FETCH',
SUBMIT = 'SUBMIT'
}type SubmitAction = {
type: typeof ActionTypes.SUBMIT;
};const submitAction: SubmitAction = {
type: ActionTypes.SUBMIT
};

For a specific container, we can compose all these actions into just one type and use it for the reducer parameter type.

对于特定的容器,我们可以将所有这些动作组合为一种类型,并将其用于reducer参数类型。

It would look like this:

它看起来像这样:

type Actions = FetchAction | SubmitAction;function reducer(state, action: Actions) {
switch (action.type) {
case ActionTypes.FETCH:
// fetching action
case ActionTypes.SUBMIT:
// submiting action
}
}

All the possible actions are the Actions type. And we use a union type to "join" all action types. The action in the reducer can have the FetchAction or the SubmitAction.

所有可能的操作都是“ Actions类型。 并且我们使用联合类型来“联接”所有动作类型。 减速器中的操作可以具有FetchActionSubmitAction

As a Potterhead, I couldn’t miss a Harry Potter example. I want to build a simple function to choose a Hogwarts House based on the person trait. Let’s start with the houses first.

作为Potterhead,我不能错过Harry Potter的例子。 我想建立一个简单的功能,根据人的特质选择霍格沃茨之家。 让我们先从房屋开始。

type House = {
name: string;
traits: string[];
}const gryffindor: House = {
name: 'Gryffindor',
traits: ['courage', 'bravery']
};const slytherin: House = {
name: 'Slytherin',
traits: ['ambition', 'leadership']
};const ravenclaw: House = {
name: 'Ravenclaw',
traits: ['intelligence', 'learning']
};const hufflepuff: House = {
name: 'Hufflepuff',
traits: ['hard work', 'patience']
};const houses: House[] = [
gryffindor,
slytherin,
ravenclaw,
hufflepuff
];

I want to keep it simple, so the House type has only the name and the traits, a list of possible traits from people related to the house.

我想保持简单,因此“ House类型仅包含nametraits ,这是与房屋有关的人员可能的特征的列表。

And then, I create each house and added all of them to the houses list.

然后,我创建每个房屋并将所有房屋添加到houses列表中。

Great! Now I’ll build the Person type. A person can be a witch or a muggle.

大! 现在,我将构建Person类型。 一个人可以是巫婆或麻瓜。

type Witch = {
name: string;
trait: string;
magicFamily: string;
}type Muggle = {
name: string;
trait: string;
email: string;
}

And this is the part we combine these two different types using the union type:

这是我们使用并集类型将这两种不同类型结合在一起的部分:

type Person = Muggle | Witch;

Using the intersection type, the Person type has all properties from Muggle or all from Witch.

使用交集类型,“ Person类型具有Muggle所有属性或Witch所有属性。

So now, if I create a Muggle, I need just the name, the trait, and the email:

所以现在,如果我创建一个Muggle ,我只需要名称,特征和电子邮件:

const hermione: Muggle = {
name: 'Hermione Granger',
trait: 'bravery',
email: 'hermione@mail.com'
};

If I create a Witch, I need the name, the trait, and the magic family name:

如果创建Witch ,则需要名称,特征和魔术家族名称:

const harry: Witch = {
name: 'Harry Potter',
trait: 'courage',
magicFamily: 'Potter'
};

And if I create a Person, I need at least the name and the trait properties from Muggle and Witch:

如果创建一个Person ,则至少需要MuggleWitchnametrait属性:

const tk: Person = {
name: 'TK',
email: 'tk@mail.com',
trait: 'learning',
magicFamily: 'Kinoshita'
};

The chooseHouse is very simple. We just pas the houses and the person. Based on the person trait, the function will return the chosen house:

chooseHouse非常简单。 我们只是贴房屋和人。 基于人的特质,该函数将返回所选房屋:

function chooseHouse(houses: House[], person: Person) {
return houses.find((house) => house.traits.includes(person.trait))
}

And applying all the people we created:

并应用我们创建的所有人员:

chooseHouse(houses, harry); // { name: 'Gryffindor', traits: ['courage', 'bravery'] }
chooseHouse(houses, hermione); // { name: 'Gryffindor', traits: ['courage', 'bravery'] }
chooseHouse(houses, tk); // { name: 'Ravenclaw', traits: ['intelligence', 'learning'] }

Nice!

真好!

The intersection type is a bit different, but it can also be used to combine existing types.

交集类型有所不同,但也可以用于合并现有类型。

When I was implementing a web app to apply my studies on UX, I needed to create a prop type for the Image component.

当我实现一个Web应用程序以将我的研究应用于UX时 ,我需要为Image组件创建prop类型。

I had the type ImageUrl from the product type:

我的产品类型为ImageUrl

type ImageUrl = {
imageUrl: string;
};

And the ImageAttr to represent all the attributes for the image:

ImageAttr代表图像的所有属性:

type ImageAttr = {
imageAlt: string;
width?: string
};

But the props expected all this information in the component. Intersection type for the rescue!

但是道具期望组件中包含所有这些信息。 救援的交叉口类型!

type ImageProps = ImageUrl & ImageAttr;

Simple as that. So now, the component needs all these properties. The type looks like this:

就那么简单。 因此,现在组件需要所有这些属性。 类型看起来像这样:

type ImageProps = {
imageUrl: string;
imageAlt: string;
width?: string
};

And we can use this type this way:

我们可以这样使用这种类型:

const imageProps: ImageProps = {
imageUrl: 'www.image.com',
imageAlt: 'an image',
};const imagePropsWithWidth: ImageProps = {
imageUrl: 'www.image.com',
imageAlt: 'an image',
width: '100%'
};

Nice! One more concept to reuse and compose types.

真好! 重用和组合类型的另一种概念。

I also find the Pick type very interesting and useful. We have other interesting types that we could write here, but the idea here is to understand that we can compose type and there is no limit to reuse types. If you're interested in study other types, take a look at this post I wrote: Typescript Learnings: Interesting Types.

我还发现Pick类型非常有趣且有用。 我们还有其他有趣的类型可以在这里编写,但是这里的想法是要理解我们可以组成类型,并且对重用类型没有限制。 如果您有兴趣研究其他类型,请查看我写的这篇文章: Typescript Learnings:有趣的类型

工装 (Tooling)

When you npm install typescript, you don't just get the compiler, you get the language service API, a standalone server called tsserver that editors can run to provide autocompletion, go-to, and other cool features.

当您npm install typescript ,您不仅获得了编译器,还获得了语言服务API,这是一个名为tsserver的独立服务器,编辑器可以运行该服务器以提供自动完成功能,转到和其他出色功能。

These features are what some people from the Typescript team call developer productivity tools like smart errors when type checking and IntelliSense (code completion, hover info, signature information). We look at these features throughout the whole article, but I want to make a special topic to talk about it.

这些功能就是Typescript团队中的一些人所说的开发人员生产力工具,例如类型检查和IntelliSense时的智能错误(代码完成,悬停信息,签名信息)。 我们将在整篇文章中介绍这些功能,但我想专门讨论一下。

The Typescript type checker is powerful in the sense that it can infer types and provide information to some possible issues. Example: It inferred that the city is a string. And the uppercase is used the wrong way. As it knows it is a string, it also tries to find a possible method that the engineer is looking for.

Typescript类型检查器在可以推断类型并为某些可能的问题提供信息的意义上是强大的。 示例:推断城市是一个字符串。 uppercase使用错误的方式。 知道它是一个字符串,它还尝试找到工程师正在寻找的可能方法。

const city = 'Tokyo';
city.toUppercase();
// Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?

In this case, the compiler is really smart, because it finds exatcly what we wanted.

在这种情况下,编译器真的很聪明,因为它可以找到我们想要的东西。

It also works for objects:

它也适用于对象:

const people = [
{ name: 'TK', age: 24 },
{ name: 'Kaio', age: 12 },
{ name: 'Kazumi', age: 31 },
];for (const person of people) {
console.log(person.agi);
// Property 'agi' does not exist on type '{ name: string; age: number; }'
}

With the static types, the tooling can provide a great developer experience with code completion, hover info to show defined types, and signature information for methods and other data.

使用静态类型,该工具可以为开发人员提供出色的代码完成,悬停信息以显示定义的类型以及方法和其他数据的签名信息的出色开发经验。

If you type: 'TK'., the editor will show all the possible methods for the string object. The compiler knows it is a string. And it knows the methods from the String prototype. But it also provides the method signature. This is very interesting because we don't necessarily need to go to the docs. The "docs" is already in our code editor.

如果输入: 'TK'. ,编辑器将显示该字符串对象的所有可能方法。 编译器知道它是一个字符串。 而且它知道String原型中的方法。 但它也提供方法签名。 这非常有趣,因为我们不一定需要去看文档。 “文档”已经在我们的代码编辑器中。

It’s an awesome experience while coding.

这是编码时的绝佳体验。

The type definition “on hover” is another thing that we saw earlier in this article. Let the compiler infer the types implicitly and you won’t lose the type documentation. Using the hover in the object, the IDE or editor will always be able to show the type definition.

“悬停时”的类型定义是我们在本文前面看到的另一件事。 让编译器隐式推断类型,您将不会丢失类型文档。 使用对象中的悬停,IDE或编辑器将始终能够显示类型定义。

Another interesting thing is that Typescript will not only flag what could go wrong on runtime, but it also helps to find code that doesn’t do what you intend.

另一个有趣的事情是Typescript不仅会标记运行时可能出错的地方,而且还有助于查找无法达到您期望的代码。

Imagine we have a function to open a snackbar if it is still closed. It would verify the status of the snackbar. If it is closed, just call another function to open it.

想象一下,如果小吃店仍然关闭,我们可以打开它。 它将验证小吃店的状态。 如果关闭,只需调用另一个函数即可将其打开。

const buildSnackbar = (status: SnackbarStatus) => {
if (status.isClosed) {
openSnackbar();
}
};

And the type information for this snackbar is:

这个小吃店的类型信息是:

type SnackbarStatus = {
isClosed: boolean;
};

What happens if I call this function like this:

如果我这样调用此函数会发生什么:

buildSnackbar({ isclosed: true });

It won’t break in runtime, because the status object has no isClosed attribute and the undefined object is a falsy value, so it will skip the if condition and not call the openSnackbar function. No runtime error. But probably it will behavior different than the expected.

它不会在运行时中断,因为status对象没有isClosed属性,而undefined对象是falsy值,因此它将跳过if条件,而不调用openSnackbar函数。 没有运行时错误。 但是可能行为与预期不同。

In Typescript, the compiler will give some hints to make it works properly. First it will show this error:

在Typescript中,编译器将给出一些提示,以使其正常工作。 首先,它将显示此错误:

// Argument of type '{ isclosed: boolean; }' is not assignable to
// parameter of type 'SnackbarStatus'.

isclosed with downcased C is not assignable to the type. It's not defined there. This is the first hint to make you correct your code.

isclosed与downcased C不可分配的类型。 在那里没有定义。 这是使您更正代码的第一个提示。

The second is even better:

第二个更好:

// Object literal may only specify known properties,
// but 'isclosed' does not exist in type 'SnackbarStatus'.
// Did you mean to write 'isClosed'?

It tells exactly what you probably need to do: rename the isclosed to isClosed.

它确切地告诉您您可能需要做什么:将isclosed重命名为isClosed

We can talk a lot of things about the tooling about I think this is the main part.

我们可以谈论有关工具的很多事情,我认为这是主要部分。

My suggestion to learn more about this is to just code in Typescript and “have a conversation” with the compiler. Read the errors. Play with the hover. See the autocompletion. Understand the method signatures. It’s really a productive way to code.

我建议了解更多有关此的建议是仅使用Typescript进行编码,然后与编译器进行“对话”。 阅读错误。 玩悬停。 请参阅自动完成。 了解方法签名。 这确实是一种高效的编码方式。

提示与学习 (Tips & Learnings)

As the article is coming to an end, I want to just add the final thoughts, learnings, and tips to help you in your journey learning Typescript or just applying it in your projects.

在本文即将结束时,我只想添加最终的思想,学习和技巧,以帮助您学习Typescript或将其应用到项目中。

  • Really read the type error: this will help you better understand the issue and the types.

    真正阅读类型错误:这将帮助您更好地理解问题和类型。
  • strictNullChecks and noImplicitAny can be very helpful in finding bugs. Enable this as soon as possible in your project. Use strictNullChecks to prevent “undefined is not an object”-style runtime errors. Use noImplicitAny to type the source code to give more type information for the compiler.

    strictNullChecksnoImplicitAny对发现错误很有帮助。 在您的项目中尽快启用它。 使用strictNullChecks可以防止出现“未定义不是对象”样式的运行时错误。 使用noImplicitAny键入源代码可为编译器提供更多类型信息。

  • Together with these compiler’s configurations, I always recommend being very precise about your types. Mainly with the values that occur only in runtime like an API response. Correctness is important to catch as many bugs as possible in compile time.

    连同这些编译器的配置一起,我始终建议您对类型非常精确。 主要带有仅在运行时出现的值,例如API响应。 正确性对于在编译时捕获尽可能多的错误很重要。
  • Understand the difference between runtime and compile time: types only affects in compile type. It runs the type checker and then compiles to Javascript. The Javascript source code doesn’t use any type of references or type operations.

    了解运行时和编译时间之间的区别:类型仅影响编译类型。 它运行类型检查器,然后编译为Javascript。 Javascript源代码不使用任何类型的引用或类型操作。
  • Learn about utility types. We talk about more specific about the Readonly in the immutability in compile time, but Typescript has a box of helpers like Required, Pick, and many more.

    了解实用程序类型。 我们在编译时讨论了不可变性方面的Readonly ,但是Typescript带有一盒帮助器,例如RequiredPick等。

  • If possible, prefer letting the compiler infers the types for you. Most of the types and returning types are redundant. The Typescript compiler is very smart in this topic. If not possible, you can always add type annotations. And let the type assertions as to the last option.

    如果可能,最好让编译器为您推断类型。 大多数类型和返回类型都是多余的。 Typescript编译器在此主题中非常聪明。 如果不可能,您始终可以添加类型注释。 并让类型断言有关最后一个选项。
  • As writing code, take a look at the tooling. The design of the tooling provided in an IDE is amazing. The IntelliSense and type checking provide a really good experience.

    在编写代码时,请看一下工具。 IDE中提供的工具设计令人惊叹。 IntelliSense和类型检查提供了非常好的体验。

资源资源 (Resources)

I compiled (pun very much intended!) a bunch of resources to help you learn more about programming languages, type systems, and the type mental model.

我编译了(非常有意!)大量资源,以帮助您了解有关编程语言,类型系统和类型思维模型的更多信息。

Also, if you found the examples on this post useful, I added all of them this repository: Thinking in Types. So you can fork and play with it.

另外,如果您发现这篇文章中的示例很有用,那么我将所有这些都添加到了该存储库: 思维类型 。 因此,您可以进行分叉操作。

类型系统 (Type Systems)

工具和开发人员经验 (Tooling & Developer Experience)

编译时间与运行时 (Compile time vs Runtime)

最佳实践 (Best Practices)

翻译自: https://medium.com/the-renaissance-developer/a-mental-model-to-think-in-typesscript-c5e1b319c7bf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值