从JavaScript到TypeScript,Pt。 IV:泛型和代数数据类型

They don't get as much love as classes, but TypeScript's generics and fancier type features are some of its most powerful design features of the language.

它们没有像类那么受宠,但是TypeScript的泛型和更高级的类型功能是该语言最强大的设计功能之一。

If you've got a background in Java, you'll be familiar with how much of a splash they made when they first came around. Similarly, if you've written any Elm, you'll be right at home with how TypeScript expresses algebraic data types.

如果您具有Java的背景知识,您将熟悉它们在刚出现时引起的轰动。 同样,如果您编写了任何Elm ,那么TypeScript如何表达代数数据类型将是您的家。

This article will cover:

本文将介绍:

  • Union & Intersection Types;

    联合和交叉点类型;
  • Type Aliases & Type Guards; and

    类型别名和类型防护; 和
  • Generics.

    泛型。

I expect you're using Node to run the code samples; I'm running v6.0.0. To get the code, clone the repo and checkout the Part_4 branch:

我希望您正在使用Node运行代码示例; 我正在运行v6.0.0。 要获取代码,请克隆存储库并检出Part_4分支:

git clone https://github.com/Peleke/oop_in_typescript
git checkout Part_4_Generics_and_More_Types

Running tsc will create a directory called built, in which you'll find the compiled JavaScript. The compiler will spit out a window's worth of errors, but you can safely ignore them.

运行tsc将创建一个名为built ,在其中您会发现编译JavaScript。 编译器将吐出一个窗口的错误值,但是您可以放心地忽略它们。

联合类型 ( Union Types )

TypeScript lets us declare that a variable is a string, number, or anything else. It also lets us create a list of types, and declare that a variable is allowed to have any of the types in that list. You do this with a union type.

TypeScript让我们声明变量是stringnumber或其他任何东西。 它还使我们可以创建类型列表,并声明允许变量具有该列表中的任何类型。 您可以使用并集类型执行此操作。

A ** union type ** is somewhere between a specific type and any. A ** union type gives a list of types that that a value can have **.

** 联合类型 **位于特定类型和any类型之间。 **联合类型提供值可以具有**的类型的列表。

A variable with a union type (A | B) can hold a value of type A, or of type B. The pipe character tells TypeScript we should be happy whichever one we get.

联合类型(A | B)的变量可以包含类型A 类型B 。 管道字符告诉TypeScript,无论我们得到哪个,我们都应该感到高兴。

The *"Hello, world"* of union types is a function that accepts either a string or a number:

联合类型的*“你好,世界” *是接受一个字符串数字的功能:

// basic_union.ts

"use strict";

// foo can print either numbers or strings without issue.
function foo (bar : (string | number)) : void {
    console.log(`foo says ${bar}.`);
}

foo('Hello, world');
foo(42);

Instead of writing (string | number) everywhere, we can rename it with a ** type alias **:

不用在任何地方写(string | number) ,我们可以使用**类型别名**重命名它:

// basic_unions_alias.ts

"use strict";

// Parentheses aren't required in type alias definitions.
type Alphanumeric = string | number;

function foo (bar : Alphanumeric) : void {
    console.log(`foo says ${bar}.`);
}

foo('Hello, world');
foo(42);

That's all there is to it.

这里的所有都是它的。

One important caveat is that you can't use type aliases recursively. That means you can't do things like this:

一个重要的警告是您不能递归使用类型别名。 这意味着您不能做这样的事情:

"use strict";

type Tree = Empty | Leaf | Tree; // This is invalid . . . 
type Alphanumeric = string | number;

type Empty = void;
type Leaf = Alphanumeric;
type TreeNode = [Tree, Tree] // . . . And this is, too.

This is a typical way to define recursive data structures in certain functional programming languages, but aliases and unions aren't as robust in TypeScript.

这是在某些功能编程语言中定义递归数据结构的典型方法,但是别名和联合在TypeScript中不那么可靠。

交叉点类型 ( Intersection Types )

Intersections are essentially the opposite of unions. Whereas a union type can hold any of the types in its list, an intersection type can only hold a value that is simultaneously an instance of every type in its list. You define them like union types, but replace the pipe with an &.

交叉点基本上是联合的对立面。 联合类型可以包含其列表中的任何类型,而交集类型只能包含一个值,该值同时是其列表中每种类型的实例。 您可以像联合类型一样定义它们,但是用&替换管道。

"use strict";

// Any object of type Enjoyable must have both a 
//   'drink' and a 'smoke' method.
type Enjoyable = Drinkable & Smokable;

interface Drinkable {
    drink : (() => void);
}

interface Smokable {
    smoke : (() => void);
}

While uncommon, interfaces are powerful tools for guaranteeing that an object implements some set of interfaces. TypeScript's inference is pretty good most of the time, but sometimes you want a rock-solid check. When that happens, you're stuck running a series of property checks. Intersections cleans all that right up.

接口虽然很少见,但却是功能强大的工具,可确保对象实现某些接口集。 在大多数情况下,TypeScript的推断是相当不错的,但有时您需要进行严格的检查。 发生这种情况时,您将无法进行一系列属性检查。 交叉路口会清理所有东西。

In the skeleton below, handleRefresh manages common cacheing and view update logic for Updatabale & Cacheable widgets.

在下面的框架中, handleRefresh管理Updatabale & Cacheable小部件的通用缓存和视图更新逻辑。

I could also type it to accept a Widget, but then I wouldn't be able to use Updatable & Cacheable objects from arbitrary hierarchies. That doesn't matter right now, but when I change things down the line -- which I inevitably will -- the flexibility will make the code much less brittle.

我也可以键入它来接受Widget ,但是那样我将无法使用任意层次结构中的Updatable & Cacheable对象。 现在这无关紧要,但是当我改变生产线时(我不可避免地会改变这一点),灵活性将使代码的脆性大大降低。

// intersection.ts

"use strict";

type DynamicWidget = Updatable & Cacheable;

function handleRefresh (widget : DynamicWidget) : void {
  // Update widget's cache, and inform listening views that it's time to 
  //   update the UI.
}

// Interfaces
// ===============================================
interface Updatable {

  // SettingsWidget changes user settings; VocabularyWidget fetches new 
  //   translations.
  update : (() => void);
}

interface Cacheable {

  // This is an example of where interfaces shine. Both widgets below are
  //   cacheable, but each takes a totally different approach to caching:
  //   VocabularyWidget does so on a session basis, whereas
  //   SettingsWidget saves user preferences to local storage
  cache : (() => void)

}

// Utility Class
// ===============================================
class User { /* Omitted for brevity */  }

// Widget classes
// ===============================================
abstract class Widget { /* Omitted for brevity */ }

class VocabularyWidget extends Widget implements Updatable, Cacheable {

  constructor(private user : User, private words : String[]) { super() }

  cache () : void {
    // Cache API responses . . .
  }

  /* Remainder omitted */

}

class SettingsWidget extends Widget implements Updatable, Cacheable {

  constructor(private user : User) { super() }

  cache () : void {
    // Save user preferences to local storage . ..  
  }

  /* Remainder omitted */
}

While I happened to use it here, intersection types are generally rare. I usually use them as a form of documentation. Otherwise, there are few things you can do with an intersection type that you can't accomplish through some other, equally readable means.

当我碰巧在这里使用它时,交集类型通常很少见。 我通常将它们用作文档形式。 否则,使用交叉点类型可以执行的操作很少,而这些操作是您无法通过其他同样可读的方式完成的。

类型防护 ( Type Guards )

What if handleRefresh took a union type of Cacheable or Updatable, and executed different logic depending on which it got?

如果handleRefresh采用Cacheable Updatable的联合类型,并根据获取的内容执行不同的逻辑该怎么办?

Well, to be frank, I'd probably rewrite the code. But sometimes it's convenient to write a function that accepts a union and branches based on which type it actually receives.

好吧,坦率地说,我可能会重写代码。 但是有时候编写一个接受联合并根据其实际接收的类型进行分支的函数很方便。

TypeScript has a handy tool for this sutation, called the ** type guard **. A type guard is a functino that makes sure a value has the properties you'd expect of that type.

TypeScript有一个方便的工具用于这种缝合,称为** typeguard **。 类型防护是一个函数,可确保值具有该类型所需的属性。

If it does, it confirms that the value is of the tested type. If it doesn't, it confirms that the value is not of the tested type, and so has to be of one of the others. TypeScript uses that extra information to simplify conditional statements.

如果是,则确认该值是测试类型。 如果不是,它将确认该值不是测试类型,因此必须是其他值之一。 TypeScript使用这些额外的信息来简化条件语句。

用户定义的类型防护 ( User-Defined Type Guards )

A type guard is just a function that accepts a union type over the types you need to check for.

类型防护只是接受您需要检查的类型的联合类型的函数。

// type_guards.ts

"use strict";

function handleRefresh(widget : (Updatable | Refreshable)) : void {
    if (isRefreshable(widget)) {
        // widget.update();
        // publish update notifications . . . 
    }
    else { 
        // widget.cache();
        // handle caching logic . . . 
    }
}

function isRefreshable(widget : (Refreshable | Cacheable)) : widget is Refreshable {
    // Cast widget to Refreshable; check for defining property.
    return (<Refreshable> widget).update !== undefined;
}

function isCacheable(widget : (Refreshable | Cacheable)) : widget is Cacheable {
    // Cast widget to Cacheable; check for defining property.
    return (<Cacheable> widget).cache !== undefined;
}

Only two things are new:

只有两件事是新的:

  1. Its return type is a ** type predicate **.

    它的返回类型是**类型谓词**。
  2. You must cast the argument to the type that you'd expect to have the property you're checking for.

    您必须将参数强制转换为希望具有要检查的属性的类型。

类型谓词 (Type Predicates)

Think of a type predicate as a special boolean value for the type system. Notice that we can just use else , rather than else if (isCacheable(widget)), and still successfully access the cache property. This is because TypeScript knows that, if our widget isn't Refreshable, it must be Cacheable. TypeScript is able to learn this from the type predicate, but wouldn't be able to simpliy things that much if we'd just returned true or false.

将类型谓词视为类型系统的特殊布尔值。 注意,我们只能使用else ,而不是else if (isCacheable(widget))else if (isCacheable(widget))仍可以成功访问cache属性。 这是因为TypeScript知道,如果我们的窗口小部件不可Refreshable ,则它必须是Cacheable 。 TypeScript可以从类型谓词中学习到这一点,但是如果我们只是返回truefalse ,那么它就无法简化太多事情。

类型转换 (Typecasting)

Casting the widget to the type we're testing against forces TypeScript to allow the property access. If we don't do the cast, it treats the value as an element of the union type you passed in -- here, (Updatable | Cacheable) -- which won't have the property you're looking for. This causes an error, which we eliminate with the cast.

widget强制转换为我们要测试的类型,强制TypeScript允许属性访问。 如果我们不进行强制类型转换,则它将值视为您传入的联合类型的元素-在这里, (Updatable | Cacheable) -不会具有您要查找的属性。 这会导致错误,我们可以通过强制转换消除错误。

We could have written this example differently:

我们可以用不同的方式写这个例子:

function handleRefresh(widget : (Updatable | Refreshable)) : void {
    if ((<Refreshable> widget).refresh !== undefined) {
        // widget.update();
        // publish update notifications . . . 
    }
    else if ((<Cacheable> widget).cache !== undefined) { 
        // widget.cache();
        // handle caching logic . . . 
    }
}

. . . But that would be brittle and verbose. And no one likes brittle code . . . Well, except for some people.

。 。 。 但这将是脆弱而冗长的。 而且没有人喜欢脆弱的代码。 。 。 好吧, 除了某些人

原始卫队 ( Primitive Guards )

If you're checking against the values of a union type over certain primitives, you don't have to write a type guard like this. You can just use the typeof functino directly.

如果要通过某些原语检查联合类型的值,则不必编写这样的类型保护。 您可以直接使用typeof函数。

// primitive_type_guards.ts

"use strict";

type Alphanumeric = string | number;

function echoType (message : Alphanumeric) : void {
    if (typeof message === "string")
        console.log(`You sent me a message: '${message}'!`);
    else
        console.log(`You sent me your number: ${message}!`);
}

echoType("You're amazing"); // "You sent me a message: 'You're amazing'!"
echoType(42);

You can use typeof like this with four primitive types:

您可以将typeof与以下四个基本类型一起使用:

  1. String;

    串;
  2. Number;

    数;
  3. Boolean; and

    布尔值 和
  4. Symbol.

    符号

A useful related idiom is the ** string literal type **, which allows you to define a type that must have one of a given set of string values. The official example for this is excellent:

有用的相关习惯用法是**字符串文字类型**,它使您可以定义必须具有一组给定字符串值之一的类型。 官方的例子很好:

"use strict";

// Define a series of string constants
type Easing = "ease-in" | "ease-out" | "ease-in-out";

class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
    // Use typeguards to branch through options . . . 
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
        // . . . Or throw, if the user passed a malformed value.
            throw new Error(`Error: ${easing} is not allowed here.`);
        }
    }
}

泛型 ( Generics )

Unions let us write more general code than specific types do. There are a few fundamental shortcomings with using unions to describe general types, though.

联合使我们可以编写比特定类型更通用的代码。 但是,使用并集描述通用类型存在一些基本缺陷。

  1. You have to know the types beforehand to define the union properly.

    您必须事先知道类型才能正确定义联合。
  2. Writing typeguards and conditional logic is brittle and verbose.

    编写类型卫士和条件逻辑是脆弱而冗长的。

These both prevent union types from being a sufficient solution to problems requiring full generality. They're general, but only in a very specific way.

这两者都使联合类型无法充分解决需要全面概括的问题。 它们是通用的,但仅以非常特定的方式。

One solution is to use any. But this throws away type checking entirely.

一种解决方案是使用any 。 但这完全丢掉了类型检查。

What if we want to create a specialized collection data type specifically for children of one of our base classes? Defining the collection in terms of any works, in that we'll be able to stuff those objects into the collection, but we'd have to check that each object comes from the right classes ourselves.

如果我们想为我们的一个基类的子类创建专门的收集数据类型怎么办? 用any 作品来定义集合,因为我们可以将那些对象填充到集合中,但是我们必须检查每个对象本身是否来自正确的类。

TypeScript's solution to the problem is ** generics **. Generics allow you to define functions, classes, and interfaces that work in general, while preserving some of the typechecks that any throws away.

TypeScript解决此问题的方法是**泛型**。 泛型使您可以定义通常可以使用的函数,类和接口,同时保留一些any抛出的类型检查。

简单泛型 ( Simple Generics )

Let's see what this looks like.

让我们看看它是什么样子。

// simple_generic.ts

"use strict";

// The general formula is:
// FUNCTION_NAME < TYPE_VARIABLE> (args) : RETURN_TYPE { FUNCTION_BODY }
// It's the same thing you're used to, plus the <TYPE_VARIABLE> bit.
function identity1<T>(input : T) : T {
  // This works, because we can return "input" no matter what it is.
  return input;
}

// We could have used any type variable; there's nothing special about T.
function identity2<E>(input : E) : string {
  // This works, because everything in JavaScript can call 
  //   Object.prototype.toString.
  return input.toString();
}

// The variable T means, "single element with some random type."
//   It can't be, "literally any value, including maybe an array, map, or set."
// function getMixedContents1<T>(input : T) : T {
//   let contents : T[] = [];
// 
//   // This doesn't work. Only Array has a forEach. What if we're passed a string?
//   input.forEach((val) => contents.push(val))
// 
//   return contents;
// }

function getMixedContents2<T>(input : T[]) : T[] {
  let contents : T[] = [];

  // This works, because all arrays have a forEach.
  input.forEach((val) => contents.push(val))

  return contents;
}

To use a generic type within a function, define your function as before, and add <TYPE_VARIABLE> right before the arguments list. Once you've done that, you can use TYPE_VARIABLE throughout the signature or function body as you would any other type.

要在函数中使用泛型类型,请像以前一样定义函数,并在参数列表之前添加<TYPE_VARIABLE> 。 完成此操作后,您可以像使用其他任何类型一样在整个签名或函数主体中使用TYPE_VARIABLE

The variable T is traditional -- it stands for Type -- but you can use anything, s long as you're consistent. Some people use E for elements in a collection, for instance.

变量T是传统变量(代表T ype),但是只要您保持一致,就可以使用任何变量。 例如,有人将E用于集合中的元素。

展开identity功能 (Unwrapping the identity Functions)

Note that identity1 expects an argument of type T, and returns a variable of type T. There's no reason we have to return the same thing we accept, though: identity2 takes a T and returns a string.

请注意, identity1期望使用类型T的参数,并返回类型T的变量。 不过,我们没有理由必须返回我们接受的相同内容: identity2接受T并返回string

identity2 is a good example of generic type checking at work. The code compiles, but not before TypeScript checks to make sure input has a toString property. Our generic could be anything, so we're not allowed to access properties we can't guarantee we'll find.

identity2是工作中通用类型检查的一个很好的例子。 代码会编译,但不会在TypeScript检查以确保input具有toString属性之前进行编译。 我们的泛型可以是任何东西,因此我们不允许访问我们无法保证会找到的属性。

But every object in JavaScript delegates a toString property lookup to Object.prototype. This is how TypeScript satisfies itself that the toString call won't throw at runtime.

但是JavaScript中的每个对象都将toString属性查找委托给Object.prototype 。 这就是TypeScript满足于toString调用在运行时不会抛出的方式。

通用数组 ( Generic Arrays )

getMixedContents1 won't compile, because it calls forEach on a generic input.

getMixedContents1 无法编译,因为它在通用input上调用forEach

We can do this with an array, but not in general: If someone passes a string, it won't have forEach.

我们可以使用数组来完成此操作,但通常不能这样做:如果有人传递了字符串,则它将没有forEach

The problem is easy to fix, though: Just give input a T[] type. Again, you can use T like any other type -- either as an indidual element, or as a constituent of a collection.

但是,该问题很容易解决:只需为input提供T[]类型。 同样,您可以像使用其他任何类型一样使用T作为单个元素或作为集合的组成部分。

This is an important point: Generics are a sort of "anything" value, but they're * individual anything values **. A generic can't be a thing *or a list of things.

这一点很重要:泛型是一种“任何东西”的值,但是它们是*任何东西值**。 泛型不能是事物*或事物列表。

These are good examples of the difference between generics and any . You can do whatever you want to an any type -- TypeScript won't check property access or method calls, because it assumes you know what you're doing. On the other hand, using a generic ensures TypeScript will prevent you from doing anything that isn't valid on every possible input. This way, you can be sure your code actually is as generic as you intend!

这些都是泛型和any区别的好例子。 您可以对any类型执行any -TypeScript不会检查属性访问或方法调用,因为它假定您知道自己在做什么。 另一方面,使用泛型可确保TypeScript阻止您对所有可能的输入进行无效的操作。 这样,您可以确保您的代码实际上与您期望的一样通用!

通用接口 ( Generic Interfaces )

TypeScript also allows you to use generics when defining method signatures and data members in an interface.

TypeScript还允许您在接口中定义方法签名和数据成员时使用泛型。

Let's say each widget I have on a page has an associated DataManager, which handles caching and refreshes. We can bundle those methods into an interface exposing two methods: cache and refresh.

假设我在页面上拥有的每个小部件都有一个关联的DataManager ,用于处理缓存和刷新。 我们可以将这些方法捆绑到一个接口中,以提供两种方法: cacherefresh

cache will either receive data to cache, or already have access to it, depending on whose DataManager we're dealing with. cache should take a generic argument, so it can receive any sort of data at all.

cache将接收要缓存的数据,或者已经可以访问它,具体取决于我们要处理的是哪个DataManagercache应采用通用参数,因此它完全可以接收任何类型的数据。

// generic.ts

"use strict";

interface DataManager {
  cache<E>( items? : E[] ) : void;
  refresh : (() => void);
}

Note that:

注意:

  1. Defining generics is exactly the same here: Function name, followed by brackets containing a type variable.

    此处定义泛型的方法完全相同:函数名称,后跟带有类型变量的方括号。
  2. The question mark after items indicates that it's an optional argument. Implementers can ignore it if they want.

    items后面的问号表示它是可选参数。 实施者可以根据需要忽略它。

The syntax for generics takes a bit of getting used to, but there's nothing surprising about how this code is written otherwise.

泛型的语法需要一点时间来适应,但是以其他方式编写此代码也就不足为奇了。

Alternatively, we could write:

或者,我们可以这样写:

interface DataManager<E> {
  cache( items? : E[] ) : void;
  refresh : (() => void);
}

In the previous example, only cache knew about E. This syntax allows the entire interface to use E. This makes sense if you use the type variable in several method signatures or data members.

在前面的示例中,只有cache知道E 此语法允许整个接口使用E 如果您在多个方法签名或数据成员中使用type变量,则这很有意义。

In our case, though, it's better to annotate cache directly; it's more precise.

不过,在我们的情况下,最好直接注释cache ; 更精确。

通用类 ( Generic Classes )

As with interfaces, you simply follow the class name with brackets containing a type variable to use generics in classes.

与接口一样,您只需在类名后面加上方括号即可,方括号中包含一个类型变量,以便在类中使用泛型。

Here, I have a View class that accepts a generic value, and assigns it as its _manager. We use the generic because we don't necessarily care if the manager formally implements the interface -- we allow it as long as it has the right methods.

在这里,我有一个View类,该类接受一个通用值,并将其分配为其_manager 。 我们之所以使用泛型,是因为我们不必关心管理器是否正式实现了接口,只要它具有正确的方法,我们就可以允许它。

// generic.ts

"use strict";

class View<T> {

  constructor (private _manager : T) { }

  get manager () : T {
    return this._manager;
  }

}

This works, but allowing anything to come through as a DataManager defeats the purpose of typing.

这可行,但是当DataManager允许任何东西通过时,键入的目的就失败了。

Instead, we can use a ** generic constraint ** specify that we'll alow any type, as long as it implements DataManager.

相反,我们可以使用**一般约束**指定只要实现了DataManager ,我们将允许任何类型。

// generic.ts

"use strict";

// The "extends" clause is our constraint. No, you can't use "implements." 
//    It must be "extends", even though we're referring to an interface.
class View<T extends DataManager> {

  constructor (private _manager : T) { }

  get manager () : T {
    return this._manager;
  }

}

If you remember the rules for structural typing, you'll realize that we could avoid generics altogether:

如果您记得结构化类型的规则,您将意识到我们可以完全避免使用泛型:

class View {

  constructor (private _manager : DataManager) { }

  get manager () : DataManager {
    return this._manager;
  }
}

Either way is acceptable, but I prefer to be explicit. I usually do less debugging that way!

两种方法都可以接受,但我希望明确。 我通常减少这种调试!

结论 ( Conclusion )

Basic types get the job done most of the time, but some things are easier with a bit more generality. Building data structures and application architectures are common examples.

基本类型大多数时候都能完成工作,但是有些通用性会更容易一些。 建立数据结构和应用程序体系结构是常见的示例。

The docs on generics and advanced types cover a few more esoteric use cases, though I've only ever found occasion to actually use what I've covered here.

关于泛型高级类型的文档涵盖了更多深奥的用例,尽管我只发现了实际使用我在此介绍的内容的机会。

As always, feel free to leave questions or comments below, or hit me on Twitter (@PelekeS) -- I'll get back to everyone indiidually!gg

与往常一样,请随时在下面留下问题或评论,或在Twitter( @PelekeS )上打我-我将单独与大家联系!gg

翻译自: https://scotch.io/tutorials/from-javascript-to-typescript-pt-iv-generics-algebraic-data-types

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值