- Prerequisites
- 预备知识
- Concepts not in Haskell
- Haskell中没有的概念
- Built-in typesa
- 内置类型
- Gradual typing
- 渐近类型
- Structural typing
- 结构类型
- Unions
- 联合
- Unit types
- 单位类型
- Concepts similar to Haskell
- Haskell中类似概念
- Contextual typing
- 上下文类型
- Type aliases
- 类型别名
- Discriminated Unions
- 可辨识联合
- Type Parameters
- 类型参数
- Module system
- 模块系统
readonlyandconstreadonly与const- Next Steps
- 下一步
TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the programmers at Microsoft could bring traditional object-oriented programs to the web. As it has developed, TypeScript’s type system has evolved to model code written by native JavaScripters. The resulting system is powerful, interesting and messy.
Typescript诞生之初,试图将传统的面向对象类型引入Javascript,这样微软的程序员就可以将传统的面向对象程序引入网络。随着它的发展,TypeScript的类型系统已经逐渐成为编写javascript代码的模式规范。由此产生的系统功能强大、有趣同时也杂乱。
This introduction is designed for working Haskell or ML programmers who want to learn TypeScript. It describes how the type system of TypeScript differs from Haskell’s type system. It also describes unique features of TypeScript’s type system that arise from its modelling of JavaScript code.
这篇介绍是为那些想学习Typescript的Haskell或ML程序员设计的。它描述了Typescript的类型系统与Haskell的类型系统的区别。它还描述了TypeScript的类型系统的独特特性,这些特性源于它对Javascript代码的规范。
This introduction does not cover object-oriented programming. In practice, object-oriented programs in TypeScript are similar to those in other popular languages with OO features.
本介绍不包括面向对象编程。在实践中,Typescript中的面向对象程序类似于那些具有OO特性的流行语言
Prerequisites
预备知识
In this introduction, I assume you know the following:
在这篇介绍中,我假设你知道以下内容:
- How to program in JavaScript, the good parts.
- 如何用JavaScript语言精粹部分编程。
- Type syntax of a C-descended language.
- c及其衍生语言的类型语法。
If you need to learn the good parts of JavaScript, read JavaScript: The Good Parts. You may be able to skip the book if you know how to write programs in a call-by-value lexically scoped language with lots of mutability and not much else. R4RS Scheme is a good example.
如果你需要学习JavaScript精粹的部分,那就去读《JavaScript语言精粹》。如果你知道如何用一种按值调用的词法作用域语言编写程序(tips:JavaScript所有函数的参数都是按值来传递的,都是副本,对象也是副本指不过该副本的指针指向同一个地址),这种语言具有很大的可变性,而其他方面则不多,那么您可能会跳过这本书。R4RS方案就是一个很好的例子。
The C++ Programming Language is a good place to learn about C-style type syntax. Unlike C++, TypeScript uses postfix types, like so: x: string instead of string x.
c++编程语言是学习C风格类型语法的好地方。与c++不同,Typeścript使用后缀类型,例如:x: string而不是string x。
Concepts not in Haskell
Haskell中没有的概念
Built-in typesa
内置类型
JavaScript defines 8 built-in types:
Javascript定义了8种内置类型:
| Type | Explanation |
|---|---|
Number |
双精度IEEE 754浮点数。 |
String |
不可变的UTF-16字符串。 |
BigInt |
任意精度格式的整数 |
Boolean |
true 和 false. |
Symbol |
通常用作键的唯一值。 |
Null |
相当于单位类型 |
Undefined |
也相当于单位类型 |
Object |
类似于文档 |
TypeScript has corresponding primitive types for the built-in types:
TypeScript有也对应的内置类型的基础类型:
numberstringbigintbooleansymbolnullundefinedobject
Other important TypeScript types
TypeScript其他重要的类型
| Type | Explanation |
|---|---|
unknown |
顶级类型 |
never |
永远达不到的类型 |
| object literal | eg { property: Type } |
void |
undefined的子类型,用作返回类型。 |
T[] |
可变数组,也写Array<T> |
[T, T] |
元组,它是固定长度但可变的 |
(t: T) => U |
函数 |
Notes:
注意点:
- Function syntax includes parameter names. This is pretty hard to get used to!
函数语法包括参数名。这是很难适应的!
let fst: (a: any, b: any) => any = (a, b) => a;// 或更精确点:let fst: <T, U>(a: T, b: U) => T = (a, b) => a;
- Object literal type syntax closely mirrors object literal value syntax:
对象字面量类型语法密切地反映了对象字面值语法:
let o: { n: number; xs: object[] } = { n: 1, xs: [] };
[T, T]is a subtype ofT[]. This is different than Haskell, where tuples are not related to lists.
[T, T]是T[]的一个子类型。这与Haskell不同,在Haskell中元组与列表不相关。
Boxed types
封装类型
JavaScript has boxed equivalents of primitive types that contain the methods that programmers associate with those types. TypeScript reflects this with, for example, the difference between the primitive type number and the boxed type Number. The boxed types are rarely needed, since their methods return primitives.
JavaScript已经封装了基本类型,包含程序员使用这些类型相关联的方法。Typescript通过例如原始类型number 和封装后的类型Number之间的区别来反映这一点。封装的类型很少用,因为它们的方法返回原始值。
(1).toExponential();// 相当于Number.prototype.toExponential.call(1);
Note that calling a method on a numeric literal requires it to be in parentheses to aid the parser.
注意,在数字上调用方法需要将其放在圆括号中,以帮助解析器。
Gradual typing
渐近类型
TypeScript uses the type any whenever it can’t tell what the type of an expression should be. Compared to Dynamic, calling any a type is an overstatement. It just turns off the type checker wherever it appears. For example, you can push any value into an any[] without marking the value in any way:
当Typescript不能确定一个表达式的类型时,它就使用any类型。与Dynamic相比,调用一个any类型都是不合理的。它会在它出现的地方关闭类型检查器。例如,你可以把任何值放入一个any[]中,而不用用任何方式标记该值:
// 在tsconfig.json文件中配置 "noImplicitAny": false, 为 false 时,如果编译器无法根据变量的使用来判断类型时,将用 any 类型代替const anys = [];anys.push(1);anys.push("oh no");anys.push({ anything: "goes" });
And you can use an expression of type any anywhere:
然后你可以在任何地方使用any类型的表达式:
anys.map(anys[1]); // oh no, "oh no" is not a function
any is contagious, too — if you initialise a variable with an expression of type any, the variable has type any too.any也会传递——如果你用any类型的表达式初始化一个变量,那么这个变量也有any类型。
let sepsis = anys[0] + anys[1]; // this could mean anything
To get an error when TypeScript produces an any, use "noImplicitAny": true, or "strict": true in tsconfig.json.
在tsconfig.json中使用"noImplicitAny": true或"strict": true来获取Typescript生成any时的错误
Structural typing
结构类型
Structural typing is a familiar concept to most functional programmers, although Haskell and most MLs are not structurally typed. Its basic form is pretty simple:
对于大多数函数式编程的程序员来说,结构类型是一个熟悉的概念,尽管Haskell和大多数MLs都不是结构类型。它的基本形式非常简单:
// @strict: falselet o = { x: "hi", extra: 1 }; // oklet o2: { x: string } = o; // ok
Here, the object literal { x: "hi", extra: 1 } has a matching literal type { x: string, extra: number }. That type is assignable to { x: string } since it has all the required properties and those properties have assignable types. The extra property doesn’t prevent assignment, it just makes it a subtype of { x: string }.
这里,对象字面量{ x: "hi", extra: 1 }有一个匹配的字面类型{ x: string, extra: number }。该类型可分配给{ x: string },因为它具有所有必需的属性,而这些属性也具有可分配的类型。额外的属性不会阻止赋值,只是使它成为{ x: string }的一个子类型。
Named types just give a name to a type; for assignability purposes there’s no difference between the type alias One and the interface type Two below. They both have a property p: string. (Type aliases behave differently from interfaces with respect to recursive definitions and type parameters, however.)
命名类型只是给类型起一个名字;出于可赋值的目的,下面的类型别名One 和下面的接口类型Two之间没有区别。它们都有一个属性p: string。(但是,在递归定义和类型参数方面,类型别名的行为与接口不同。)
type One = { p: string };interface Two {p: string;}class Three {p = "Hello";}let x: One = { p: "hi" };let two: Two = x;two = new Three();
Unions
联合
In TypeScript, union types are untagged. In other words, they are not discriminated unions like data in Haskell. However, you can often discriminate types in a union using built-in tags or other properties.
在TypeScript中,union类型是不带标签的。换句话说,它们不是像Haskell中的data那样的有区别的联合。但是,你通常可以使用内置标记或其他属性来区分联合中的类型。
function start(arg: string | string[] | (() => string) | { s: string }): string {if (typeof arg === "string") {return commonCase(arg);} else if (Array.isArray(arg)) {return arg.map(commonCase).join(",");} else if (typeof arg === "function") {return commonCase(arg());} else {return commonCase(arg.s);}function commonCase(s: string): string {return s;}}
string, Array and Function have built-in type predicates, conveniently leaving the object type for the else branch. It is possible, however, to generate unions that are difficult to differentiate at runtime. For new code, it’s best to build only discriminated unions.string、Array和Function都有内置的类型判断,方便地将其它交给对象类型。然而,在运行时很难区分联合类型。对于新规范来说,最好只生成可辨识联合。
The following types have built-in predicates:
以下类型具有内置判断:
| Type | Predicate |
|---|---|
| string | typeof s === "string" |
| number | typeof n === "number" |
| bigint | typeof m === "bigint" |
| boolean | typeof b === "boolean" |
| symbol | typeof g === "symbol" |
| undefined | typeof undefined === "undefined" |
| function | typeof f === "function" |
| array | Array.isArray(a) |
| object | typeof o === "object" |
Note that functions and arrays are objects at runtime, but have their own predicates.
注意,函数和数组在运行时是对象,但它们有自己的判断。
Intersections
交集
In addition to unions, TypeScript also has intersections:
除了联合类型之外,TypeScript还有交集:
type Combined = { a: number } & { b: string };type Conflicting = { a: number } & { a: string };
Combined has two properties, a and b, just as if they had been written as one object literal type. Intersection and union are recursive in case of conflicts, so Conflicting.a: number & string.Combined有两个属性a和b,就像它们被写成一个对象字面量类型一样。交集和并集在冲突的情况下是递归的,所以Conflicting.a: number & string。(此时a变成never永远达不到的类型)
Unit types
单位类型
Unit types are subtypes of primitive types that contain exactly one primitive value. For example, the string "foo" has the type "foo". Since JavaScript has no built-in enums, it is common to use a set of well-known strings instead. Unions of string literal types allow TypeScript to type this pattern:
单位类型是只包含一个原始值的基本类型的子类型。例如,字符串"foo"有"foo"的类型。由于JavaScript没有内置枚举,所以通常使用一组众所周知的字符串代替。字符串字面量类型的联合允许TypeScript键入以下模式:
declare function pad(s: string, n: number, direction: "left" | "right"): string;pad("hi", 10, "left");// "left" | "right"是类型!不是字符串
When needed, the compiler widens — converts to a supertype — the unit type to the primitive type, such as "foo" to string. This happens when using mutability, which can hamper some uses of mutable variables:
当需要的时候,编译器会将单位类型扩大——转换为超类型——即单位类型的基本类型,例如"foo"转成 string。在使用变量时会发生这种情况,这可能会阻碍变量的某些使用:
let s = "right";pad("hi", 10, s); // 错误: 'string' 不能赋值给 "left" | "right"。类型为'string'的参数不能赋值给类型为'"left" | "right"的参数
Here’s how the error happens:
以下是错误如何发生的:
"right": "right""right": "right"s: stringbecause"right"widens tostringon assignment to a mutable variable.s: string = "right",因为在赋值给变量时,"right"会扩大为string。stringis not assignable to"left" | "right"string是不能赋值给"left" | "right"
You can work around this with a type annotation for s, but that in turn prevents assignments to s of variables that are not of type "left" | "right".
你可以给s添加类型注解来解决这个问题,这也会阻止类型不是"left" | "right"的变量赋值给s。
let s: "left" | "right" = "right";pad("hi", 10, s);
Concepts similar to Haskell
Haskell中类似概念
Contextual typing
上下文类型
TypeScript has some obvious places where it can infer types, like variable declarations:
TypeScript有一些明显的地方可以推断类型,比如变量声明:
let s = "I'm a string!";
But it also infers types in a few other places that you may not expect if you’ve worked with other C-syntax languages:
但它也可以在其他一些地方推断类型,如果你使用过其他C语言语法,你可能不会想到:
declare function map<T, U>(f: (t: T) => U, ts: T[]): U[];let sns = map((n) => n.toString(), [1, 2, 3]); // sns类型为string[]
Here, n: number in this example also, despite the fact that T and U have not been inferred before the call. In fact, after [1,2,3] has been used to infer T=number, the return type of n => n.toString() is used to infer U=string, causing sns to have the type string[].
在这n: number这个例子中,尽管T和U在调用之前没有被推断。事实上,在[1,2,3]被用来推断T=number之后,返回类型n => n.tostring()被用来推断U=string,导致sns的类型为string[]
Note that inference will work in any order, but intellisense will only work left-to-right, so TypeScript prefers to declare map with the array first:
注意,推论可以从任何顺序开始,但一般情况下只会从左到右,所以Typescript更倾向先用数组声明map:
declare function map<T, U>(ts: T[], f: (t: T) => U): U[];
Contextual typing also works recursively through object literals, and on unit types that would otherwise be inferred as string or number. And it can infer return types from context:
上下文类型还通过对象字面量递归地工作,以及在单位类型上工作,否则将被推断为字符串或数字。它可以从上下文推断返回类型:
declare function run<T>(thunk: (t: T) => void): T;let i: { inference: string } = run((o) => {o.inference = "INSERT STATE HERE";});
The type of o is determined to be { inference: string } becauseo的类型被确定为{inference: string},因为
- Declaration initialisers are contextually typed by the declaration’s type:
{ inference: string }.
声明的初始化式是由声明的类型上下文键入的{ inference: string }
- The return type of a call uses the contextual type for inferences, so the compiler infers that
T={ inference: string }.
返回值类型也使用上下文类型进行推断,因此编译器推断T={ inference: string }
- Arrow functions use the contextual type to type their parameters, so the compiler gives
o: { inference: string }.
箭头函数使用上下文类型来键入它们的参数,因此编译器给出o: { inference: string }。
And it does so while you are typing, so that after typing o., you get completions for the property inference, along with any other properties you’d have in a real program. Altogether, this feature can make TypeScript’s inference look a bit like a unifying type inference engine, but it is not.
它会在你键入的时候这样做,所以在输入o.之后,你会得到属性推断的补全,以及你在实际程序中会有的其他属性。总之,这个特性可以让TypeScript的推断看起来有点像一个统一的类型推断引擎,但事实并非如此。
Type aliases
类型别名
Type aliases are mere aliases, just like type in Haskell. The compiler will attempt to use the alias name wherever it was used in the source code, but does not always succeed.
类型别名只是别名,就像Haskell中的type一样。编译器将尝试在源代码中使用别名的地方使用别名,但并不总是成功。
type Size = [number, number];let x: Size = [101.1, 999.9];
The closest equivalent to newtype is a tagged intersection:
与newtype最相似的是带标记的交集:
type FString = string & { __compileTimeOnly: any };let x: FString;x.__compileTimeOnly
An FString is just like a normal string, except that the compiler thinks it has a property named __compileTimeOnly that doesn’t actually exist. This means that FString can still be assigned to string, but not the other way round.
一个FString就像一个普通的字符串,除了编译器认为它有一个名为__compileTimeOnly的属性,而这个属性实际上并不存在。这意味着FString仍然可以被赋值给string。但不是反过来。
Discriminated Unions
可辨识联合
The closest equivalent to data is a union of types with discriminant properties, normally called discriminated unions in TypeScript:
与data最接近的是带有可辨识属性的类型的联合,在TypeScript中通常称为可辨识联合:
type Shape =| { kind: "circle"; radius: number }| { kind: "square"; x: number }| { kind: "triangle"; x: number; y: number };
Unlike Haskell, the tag, or discriminant, is just a property in each object type. Each variant has an identical property with a different unit type. This is still a normal union type; the leading | is an optional part of the union type syntax. You can discriminate the members of the union using normal JavaScript code:
与Haskell不同,标签(或判别式)只是每个对象类型中的一个属性。每个都有一个相同的属性和不同的单位类型。这仍然是一个正常的联合类型;开头的|是联合类型语法的可选部分。你可以使用普通的Javascript代码区分union的成员:
type Shape =| { kind: "circle"; radius: number }| { kind: "square"; x: number }| { kind: "triangle"; x: number; y: number };function area(s: Shape) {if (s.kind === "circle") {return Math.PI * s.radius * s.radius;} else if (s.kind === "square") {return s.x * s.x;} else {return (s.x * s.y) / 2;}}
Note that the return type of area is inferred to be number because TypeScript knows the function is total. If some variant is not covered, the return type of area will be number | undefined instead.
注意,area函数的返回值类型被推断为number,因为Typescript知道这个函数是完整的。如果没有覆盖某个变量,则返回的area类型将是number | undefined。
Also, unlike Haskell, common properties show up in any union, so you can usefully discriminate multiple members of the union:
此外,与Haskell不同的是,公共属性会出现在任何联合中,所以你可以有效地区分联合的多个成员:
function height(s: Shape) {if (s.kind === "circle") {return 2 * s.radius;} else {// s.kind: "square" | "triangle"return s.x;}}
Type Parameters
类型参数
Like most C-descended languages, TypeScript requires declaration of type parameters:
和大多数C及其衍生语言一样,Typescript需要声明类型参数:
function liftArray<T>(t: T): Array<T> {return [t];}
There is no case requirement, but type parameters are conventionally single uppercase letters. Type parameters can also be constrained to a type, which behaves a bit like type class constraints:
没有大小写要求,但是类型参数通常是单个大写字母。类型参数也可以约束为一个类型,它的行为有点像类型类约束:
function firstish<T extends { length: number }>(t1: T, t2: T): T {return t1.length > t2.length ? t1 : t2;}
TypeScript can usually infer type arguments from a call based on the type of the arguments, so type arguments are usually not needed.
TypeScript通常可以从调用中根据传参的类型推断出类型参数,所以通常不需要类型参数。
Because TypeScript is structural, it doesn’t need type parameters as much as nominal systems. Specifically, they are not needed to make a function polymorphic. Type parameters should only be used to propagate type information, such as constraining parameters to be the same type:
因为Typescript是结构性的,它不像名义上系统那样需要类型参数。具体来说,它们不需要使一个函数多态。类型参数应该只用于传播类型信息,例如约束参数为相同类型:
function length<T extends ArrayLike<unknown>>(t: T): number {}function length(t: ArrayLike<unknown>): number {}
In the first length, T is not necessary; notice that it’s only referenced once, so it’s not being used to constrain the type of the return value or other parameters.
在第一个length中,T是不必要的;注意,它只被引用一次,因此它没有被用来约束返回值或其他参数的类型。
Higher-kinded types
高级类型
TypeScript does not have higher kinded types, so the following is not legal:
Typescript没有更高类型的类型,所以下面的代码是不合法的:
function length<T extends ArrayLike<unknown>, U>(m: T<U>) {}
Point-free programming
无参编程
Point-free programming — heavy use of currying and function composition — is possible in JavaScript, but can be verbose. In TypeScript, type inference often fails for point-free programs, so you’ll end up specifying type parameters instead of value parameters. The result is so verbose that it’s usually better to avoid point-free programming.
无参编程——大量使用柯里化和复合函数——这在JavaScript中是可能的,但可能很冗长。在TypeScript中,无参编程的类型推断通常会失败,所以你只能指定类型参数而不是值参数。结果是如此冗长,所以通常最好避免无参编程。
Module system
模块系统
JavaScript’s modern module syntax is a bit like Haskell’s, except that any file with import or export is implicitly a module:
Javascript的现代模块语法有点像Haskell的,除了导入或导出的任何文件都是隐式模块。
import { value, Type } from "npm-package";import { other, Types } from "./local-package";import * as prefix from "../lib/third-package";
You can also import commonjs modules — modules written using node.js’ module system:
你还可以导入commonjs模块——使用node.js编写的模块的模块系统:
import f = require("single-function-package");
You can export with an export list:
你可以导出一个列表:
export { f };function f() {return g();}function g() {} // g is not exported
Or by marking each export individually:
或单独标记每个出口:
export function f { return g() }function g() { }
The latter style is more common but both are allowed, even in the same file.
后一种方式更常见,但两种方式都是允许的,即使是在同一个文件中。
readonly and const
readonly 与const
In JavaScript, mutability is the default, although it allows variable declarations with const to declare that the reference is immutable. The referent is still mutable:
在JavaScript中,变量默认是可变的,尽管它允许用const声明变量来表明引用是不可变的。然后引用(的内容)仍然是可变的:
const a = [1, 2, 3];a.push(102); // ):a[0] = 101; // D:
TypeScript additionally has a readonly modifier for properties.
Typescript还为属性提供了一个readonly修饰符。
interface Rx {readonly x: number;}let rx: Rx = { x: 1 };rx.x = 12; // error
It also ships with a mapped type Readonly<T> that makes all properties readonly:
它还附带了一个映射类型Readonly<T>,使所有属性为readonly:
interface X {x: number;}let rx: Readonly<X> = { x: 1 };rx.x = 12; // error
And it has a specific ReadonlyArray<T> type that removes side-affecting methods and prevents writing to indices of the array, as well as special syntax for this type:
并且它有一个特殊的ReadonlyArray<T>类型,消除了方法的副作用,并防止写入数组的索引,以及这种类型的特殊语法:
let a: ReadonlyArray<number> = [1, 2, 3];let b: readonly number[] = [1, 2, 3];a.push(102); // errorb[0] = 101; // error
You can also use a const-assertion, which operates on arrays and object literals:
你也可以使用常量断言,它操作数组和对象字面量:
let a = [1, 2, 3] as const;a.push(102); // errora[0] = 101; // error
However, none of these options are the default, so they are not consistently used in TypeScript code.
但是,这些选项都不是默认选项,所以它们在TypeScript代码中使用不一致。
Next Steps
下一步
This doc is a high level overview of the syntax and types you would use in everyday code. From here you should:
这个文档是你在日常代码中使用的语法和类型的高级概述。从这里你应该:
- Read the full Handbook from start to finish (30m)
- Explore the Playground examples
