TypeScript 的诞生是为了将传统的面向对象类型引入 JavaScript,以便 Microsoft 的程序员可以将传统的面向对象程序引入 Web。随着它的发展,TypeScript 的类型系统已经发展到可以为原生 JavaScript 编写的代码建模。由此产生的系统功能强大、有趣且混乱。
¥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 的在职 Haskell 或 ML 程序员而设计。它描述了 TypeScript 的类型系统与 Haskell 的类型系统有何不同。它还描述了 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 中的面向对象程序类似于其他具有 OO 特性的流行语言中的程序。
¥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.
先决条件
¥Prerequisites
在本介绍中,我假设你了解以下内容:
¥In this introduction, I assume you know the following:
-
如何用 JavaScript 编程,好的部分。
¥How to program in JavaScript, the good parts.
-
C 语言后裔的类型语法。
¥Type syntax of a C-descended language.
如果你需要学习 JavaScript 好的部分,请阅读 JavaScript:好的部分。如果你知道如何使用具有大量可变性的按值调用词法范围语言编写程序,则可以跳过这本书。R4RS 方案 就是一个很好的例子。
¥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.
C++ 编程语言 是学习 C 风格类型语法的好地方。与 C++ 不同,TypeScript 使用后缀类型,例如:x: string
而不是 string x
。
¥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
.
Haskell 中没有的概念
¥Concepts not in Haskell
内置类型
¥Built-in types
JavaScript 定义了 8 种内置类型:
¥JavaScript defines 8 built-in types:
类型 | 解释 |
---|---|
Number |
双精度 IEEE 754 浮点数。 |
String |
一个不可变的 UTF-16 字符串。 |
BigInt |
任意精度格式的整数。 |
Boolean |
true 和 false 。 |
Symbol |
通常用作键的唯一值。 |
Null |
相当于单元类型。 |
Undefined |
也相当于单元类型。 |
Object |
类似于记录。 |
¥See the MDN page for more detail.
TypeScript 为内置类型提供了相应的基础类型:
¥TypeScript has corresponding primitive types for the built-in types:
-
number
-
string
-
bigint
-
boolean
-
symbol
-
null
-
undefined
-
object
其他重要的 TypeScript 类型
¥Other important TypeScript types
类型 | 解释 |
---|---|
unknown |
顶层类型。 |
never |
底层类型。 |
对象字面量 | 例如 { property: Type } |
void |
对于没有记录返回值的函数 |
T[] |
可变数组,也写成 Array<T> |
[T, T] |
元组,它们是固定长度但可变的 |
(t: T) => U |
函数 |
注意:
¥Notes:
-
函数语法包括参数名称。这很难习惯!
¥Function syntax includes parameter names. This is pretty hard to get used to!
tslet fst: (a: any, b: any) => any = (a, b) => a;// or more precisely:let fst: <T, U>(a: T, b: U) => T = (a, b) => a; -
对象字面量类型语法与对象字面量值语法非常相似:
¥Object literal type syntax closely mirrors object literal value syntax:
tslet o: { n: number; xs: object[] } = { n: 1, xs: [] }; -
[T, T]
是T[]
的亚型。这与 Haskell 不同,在 Haskell 中,元组与列表无关。¥
[T, T]
is a subtype ofT[]
. This is different than Haskell, where tuples are not related to lists.
盒装类型
¥Boxed types
JavaScript 具有基本类型的盒装等价物,其中包含程序员与这些类型相关联的方法。TypeScript 反映了这一点,例如,基础类型 number
和装箱类型 Number
之间的差异。很少需要盒装类型,因为它们的方法返回基础类型。
¥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.
ts
(1).toExponential();// equivalent toNumber.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 在无法确定表达式的类型时使用类型 any
。与 Dynamic
相比,将 any
称为类型是言过其实了。它只是在出现的任何地方关闭类型检查器。例如,你可以将任何值压入 any[]
而无需以任何方式标记该值:
¥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:
tsTry
// with "noImplicitAny": false in tsconfig.json, anys: any[]constanys = [];anys .push (1);anys .push ("oh no");anys .push ({anything : "goes" });
你可以在任何地方使用 any
类型的表达式:
¥And you can use an expression of type any
anywhere:
ts
anys.map(anys[1]); // oh no, "oh no" is not a function
any
也具有传染性 — 如果你使用 any
类型的表达式初始化变量,则该变量也具有 any
类型。
¥any
is contagious, too — if you initialize a variable with an
expression of type any
, the variable has type any
too.
ts
let sepsis = anys[0] + anys[1]; // this could mean anything
要在 TypeScript 生成 any
时出现错误,请在 tsconfig.json
中使用 "noImplicitAny": true
或 "strict": true
。
¥To get an error when TypeScript produces an any
, use
"noImplicitAny": true
, or "strict": true
in tsconfig.json
.
结构类型
¥Structural typing
结构类型是大多数函数式程序员熟悉的概念,尽管 Haskell 和大多数 ML 都不是结构类型的。它的基本形式非常简单:
¥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:
ts
// @strict: falselet o = { x: "hi", extra: 1 }; // oklet o2: { x: string } = o; // ok
这里,对象字面量 { x: "hi", extra: 1 }
有匹配的字面量类型 { x: string, extra: number }
。该类型可分配给 { x: string }
,因为它具有所有必需的属性,并且这些属性具有可分配的类型。额外的属性不会阻止赋值,它只是使它成为 { x: string }
的子类型。
¥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 }
.
命名类型只是给一个类型起一个名字;出于可赋值性目的,类型别名 One
和下面的接口类型 Two
之间没有区别。他们都有属性 p: 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.)
tsTry
typeOne = {p : string };interfaceTwo {p : string;}classThree {p = "Hello";}letx :One = {p : "hi" };lettwo :Two =x ;two = newThree ();
联合
¥Unions
在 TypeScript 中,联合类型是未标记的。换句话说,它们不是像 Haskell 中的 data
这样的可区分联合。但是,你通常可以使用内置标签或其他属性来区分联合中的类型。
¥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.
tsTry
functionstart (arg : string | string[] | (() => string) | {s : string }): string {// this is super common in JavaScriptif (typeofarg === "string") {returncommonCase (arg );} else if (Array .isArray (arg )) {returnarg .map (commonCase ).join (",");} else if (typeofarg === "function") {returncommonCase (arg ());} else {returncommonCase (arg .s );}functioncommonCase (s : string): string {// finally, just convert a string to another stringreturns ;}}
string
、Array
和 Function
有内置类型谓词,方便为 else
分支留下对象类型。但是,有可能生成在运行时难以区分的联合。对于新代码,最好只构建可区分的联合。
¥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.
以下类型具有内置谓词:
¥The following types have built-in predicates:
类型 | 谓词 |
---|---|
字符串 | typeof s === "string" |
数字 | typeof n === "number" |
大整数 | typeof m === "bigint" |
布尔值 | typeof b === "boolean" |
符号 | typeof g === "symbol" |
undefined | typeof undefined === "undefined" |
函数 | typeof f === "function" |
数组 | Array.isArray(a) |
对象 | typeof o === "object" |
请注意,函数和数组在运行时是对象,但有自己的谓词。
¥Note that functions and arrays are objects at runtime, but have their own predicates.
交叉
¥Intersections
除了联合,TypeScript 还有交叉:
¥In addition to unions, TypeScript also has intersections:
tsTry
typeCombined = {a : number } & {b : string };typeConflicting = {a : number } & {a : string };
Combined
有两个属性,a
和 b
,就像它们被写成一个对象字面量类型一样。交叉和联合在冲突的情况下是递归的,所以 Conflicting.a: number & 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
.
单元类型
¥Unit types
单元类型是仅包含一个基础值的基础类型的子类型。例如,字符串 "foo"
的类型为 "foo"
。由于 JavaScript 没有内置枚举,因此通常使用一组众所周知的字符串来代替。字符串字面类型的联合允许 TypeScript 键入此模式:
¥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:
tsTry
declare functionpad (s : string,n : number,direction : "left" | "right"): string;pad ("hi", 10, "left");
当需要时,编译器会扩展(转换为超类型)单元类型为基本类型,例如 "foo"
到 string
。当使用可变性时会发生这种情况,这会妨碍可变变量的某些使用:
¥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:
tsTry
lets = "right";Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.2345Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.pad ("hi", 10,); // error: 'string' is not assignable to '"left" | "right"' s
以下是错误的发生方式:
¥Here’s how the error happens:
-
"right": "right"
-
s: string
因为"right"
在分配给可变变量时扩大到string
。¥
s: string
because"right"
widens tostring
on assignment to a mutable variable. -
string
不可分配给"left" | "right"
¥
string
is not assignable to"left" | "right"
你可以使用 s
的类型注释解决此问题,但这反过来会阻止将非 "left" | "right"
类型的变量赋值给 s
。
¥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"
.
tsTry
lets : "left" | "right" = "right";pad ("hi", 10,s );
类似于 Haskell 的概念
¥Concepts similar to Haskell
上下文类型
¥Contextual typing
TypeScript 有一些明显的地方可以推断类型,比如变量声明:
¥TypeScript has some obvious places where it can infer types, like variable declarations:
tsTry
lets = "I'm a string!";
但它还在其他一些地方推断出类型,如果你使用过其他 C 语法语言,你可能不会想到这些地方:
¥But it also infers types in a few other places that you may not expect if you’ve worked with other C-syntax languages:
tsTry
declare functionmap <T ,U >(f : (t :T ) =>U ,ts :T []):U [];letsns =map ((n ) =>n .toString (), [1, 2, 3]);
此处,此示例中的 n: number
也是如此,尽管在调用之前尚未推断出 T
和 U
。实际上,在使用 [1,2,3]
推断 T=number
之后,n => n.toString()
的返回类型被用来推断 U=string
,导致 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[]
.
请注意,推断可以按任何顺序进行,但智能感知只能从左到右进行,因此 TypeScript 更喜欢先用数组声明 map
:
¥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:
tsTry
declare functionmap <T ,U >(ts :T [],f : (t :T ) =>U ):U [];
上下文类型也可以通过对象字面量递归地工作,并且可以在否则会被推断为 string
或 number
的单元类型上工作。它可以从上下文推断返回类型:
¥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:
tsTry
declare functionrun <T >(thunk : (t :T ) => void):T ;leti : {inference : string } =run ((o ) => {o .inference = "INSERT STATE HERE";});
o
的类型被确定为 { inference: string }
因为
¥The type of o
is determined to be { inference: string }
because
-
声明初始值设定项根据声明的类型进行上下文类型化:
{ inference: string }
。¥Declaration initializers are contextually typed by the declaration’s type:
{ inference: string }
. -
调用的返回类型使用上下文类型进行推断,因此编译器推断
T={ inference: string }
。¥The return type of a call uses the contextual type for inferences, so the compiler infers that
T={ inference: string }
. -
箭头函数使用上下文类型来键入它们的参数,因此编译器给出了
o: { inference: string }
。¥Arrow functions use the contextual type to type their parameters, so the compiler gives
o: { inference: string }
.
它会在你键入时执行此操作,因此在键入 o.
后,你将获得属性 inference
的完成,以及你在真实程序中拥有的任何其他属性。总而言之,这个特性可以让 TypeScript 的推断看起来有点像一个统一的类型推断引擎,但事实并非如此。
¥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.
类型别名
¥Type aliases
类型别名只是别名,就像 Haskell 中的 type
一样。编译器将尝试在源代码中使用别名的任何地方使用它,但并不总是成功。
¥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.
tsTry
typeSize = [number, number];letx :Size = [101.1, 999.9];
与 newtype
最接近的等效项是标记的交叉:
¥The closest equivalent to newtype
is a tagged intersection:
ts
type FString = string & { __compileTimeOnly: any };
FString
就像一个普通的字符串,只是编译器认为它有一个名为 __compileTimeOnly
的属性,但实际上并不存在。这意味着 FString
仍然可以分配给 string
,但反之则不行。
¥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.
判别联合
¥Discriminated Unions
与 data
最接近的等效项是具有判别属性的类型联合,在 TypeScript 中通常称为判别联合:
¥The closest equivalent to data
is a union of types with discriminant
properties, normally called discriminated unions in TypeScript:
ts
type Shape =| { kind: "circle"; radius: number }| { kind: "square"; x: number }| { kind: "triangle"; x: number; y: number };
与 Haskell 不同,标记或判别式只是每个对象类型中的一个属性。每个变体都具有相同的属性和不同的单元类型。这仍然是一个普通的联合类型;前导 |
是联合类型语法的可选部分。你可以使用普通的 JavaScript 代码来区分联合的成员:
¥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:
tsTry
typeShape =| {kind : "circle";radius : number }| {kind : "square";x : number }| {kind : "triangle";x : number;y : number };functionarea (s :Shape ) {if (s .kind === "circle") {returnMath .PI *s .radius *s .radius ;} else if (s .kind === "square") {returns .x *s .x ;} else {return (s .x *s .y ) / 2;}}
请注意,area
的返回类型被推断为 number
,因为 TypeScript 知道该函数是完整的。如果未涵盖某些变体,则 area
的返回类型将改为 number | undefined
。
¥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.
此外,与 Haskell 不同的是,公共属性出现在任何联合中,因此你可以有效地区分联合的多个成员:
¥Also, unlike Haskell, common properties show up in any union, so you can usefully discriminate multiple members of the union:
tsTry
functionheight (s :Shape ) {if (s .kind === "circle") {return 2 *s .radius ;} else {// s.kind: "square" | "triangle"returns .x ;}}
类型参数
¥Type Parameters
与大多数 C 语言后裔一样,TypeScript 需要声明类型参数:
¥Like most C-descended languages, TypeScript requires declaration of type parameters:
ts
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:
ts
function firstish<T extends { length: number }>(t1: T, t2: T): T {return t1.length > t2.length ? t1 : t2;}
TypeScript 通常可以根据参数的类型从调用中推断类型参数,因此通常不需要类型参数。
¥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:
ts
function length<T extends ArrayLike<unknown>>(t: T): number {}function length(t: ArrayLike<unknown>): number {}
第一个 length
,T 不是必须的;请注意,它只被引用一次,所以它没有被用来约束返回值或其他参数的类型。
¥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.
更高等的类型
¥Higher-kinded types
TypeScript 没有更高等的类型,所以以下是不合法的:
¥TypeScript does not have higher kinded types, so the following is not legal:
ts
function length<T extends ArrayLike<unknown>, U>(m: T<U>) {}
无点编程
¥Point-free programming
无点编程(大量使用柯里化和函数组合)在 JavaScript 中是可能的,但可能很冗长。在 TypeScript 中,对于无点程序,类型推断通常会失败,因此你最终会指定类型参数而不是值参数。结果非常冗长,通常最好避免无点编程。
¥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.
模块系统
¥Module system
JavaScript 的现代模块语法有点像 Haskell 的,除了任何带有 import
或 export
的文件都是隐式模块:
¥JavaScript’s modern module syntax is a bit like Haskell’s, except that
any file with import
or export
is implicitly a module:
ts
import { value, Type } from "npm-package";import { other, Types } from "./local-package";import * as prefix from "../lib/third-package";
你还可以导入 commonjs 模块 - 使用 Node.js 模块系统编写的模块:
¥You can also import commonjs modules — modules written using node.js’ module system:
ts
import f = require("single-function-package");
你可以使用导出列表导出:
¥You can export with an export list:
ts
export { f };function f() {return g();}function g() {} // g is not exported
或者通过单独标记每个导出:
¥Or by marking each export individually:
ts
export function f() { return g() }function g() { }
后一种风格更常见,但两者都是允许的,即使在同一个文件中也是如此。
¥The latter style is more common but both are allowed, even in the same file.
readonly
和 const
¥readonly
and const
在 JavaScript 中,可变性是默认值,尽管它允许使用 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:
js
const a = [1, 2, 3];a.push(102); // ):a[0] = 101; // D:
TypeScript 还有一个 readonly
属性修饰符。
¥TypeScript additionally has a readonly
modifier for properties.
ts
interface Rx {readonly x: number;}let rx: Rx = { x: 1 };rx.x = 12; // error
它还附带一个映射类型 Readonly<T>
,使所有属性成为 readonly
:
¥It also ships with a mapped type Readonly<T>
that makes
all properties readonly
:
ts
interface X {x: number;}let rx: Readonly<X> = { x: 1 };rx.x = 12; // error
它有一个特定的 ReadonlyArray<T>
类型,可以删除副作用方法并防止写入数组的索引,以及这种类型的特殊语法:
¥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:
ts
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:
ts
let a = [1, 2, 3] as const;a.push(102); // errora[0] = 101; // error
但是,这些选项都不是默认选项,因此它们在 TypeScript 代码中的使用并不一致。
¥However, none of these options are the default, so they are not consistently used in TypeScript code.
下一步
¥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
-
探索 在线运行示例
¥Explore the Playground examples