在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型来表示它们。
正如我们所见,它们可以是匿名的:
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}尝试
或者它们可以通过使用任何一个接口来命名
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}尝试
或类型别名。
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}尝试
在上述所有三个示例中,我们编写的函数接受包含属性name
(必须是 a string
)和age
(必须是 a number
)的对象。
属性修饰符
对象类型中的每个属性都可以指定几件事:类型、属性是否是可选的以及是否可以写入该属性。
可选属性
很多时候,我们会发现自己在处理可能具有属性集的对象。在这些情况下,我们可以通过在其名称末尾添加问号 ( ) 来将这些属性标记为可选。?
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });尝试
在此示例中,两者xPos
和yPos
都被认为是可选的。我们可以选择提供其中任何一个,因此上面的每个调用paintShape
都是有效的。所有的可选性真正说明的是,如果设置了属性,它最好有一个特定的类型。
strictNullChecks我们也可以从这些属性中读取——但是当我们在undefined
.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
(property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
(property) PaintOptions.yPos?: number | undefined
// ...
}尝试
在 JavaScript 中,即使该属性从未被设置,我们仍然可以访问它——它只是为我们提供 value undefined
。我们可以undefined
专门处理。
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
let xPos: number
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
let yPos: number
// ...
}尝试
请注意,这种为未指定值设置默认值的模式非常普遍,以至于 JavaScript 有语法来支持它。
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
(parameter) xPos: number
console.log("y coordinate at", yPos);
(parameter) yPos: number
// ...
}尝试
在这里,我们对’ 的参数使用了解构模式,并为和提供了默认值。Now和两者都肯定存在于 的正文中,但对于任何调用者来说都是可选的。paintShape``xPos``yPos``xPos``yPos``paintShape``paintShape
请注意,目前没有办法在解构模式中放置类型注释。这是因为下面的语法在 JavaScript 中已经有了不同的含义。
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);Cannot find name 'shape'. Did you mean 'Shape'?Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos);Cannot find name 'xPos'.Cannot find name 'xPos'.
}尝试
在对象解构模式中,shape: Shape
意味着“获取属性shape
并在本地将其重新定义为名为Shape
. 同样xPos: number
创建一个名为的变量number
,其值基于参数的xPos
.
使用映射修饰符,您可以删除optional
属性。
readonly
特性
属性也可以标记readonly
为 TypeScript。虽然它不会在运行时更改任何行为,但readonly
在类型检查期间无法写入标记为的属性。
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";Cannot assign to 'prop' because it is a read-only property.Cannot assign to 'prop' because it is a read-only property.
}尝试
使用readonly
修饰符并不一定意味着一个值是完全不可变的——或者换句话说,它的内部内容不能改变。这只是意味着属性本身不能被重写。
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {Cannot assign to 'resident' because it is a read-only property.Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}尝试
管理对readonly
暗示的期望很重要。在 TypeScript 的开发期间发出关于如何使用对象的意图很有用。readonly
TypeScript在检查这些类型是否兼容时不会考虑两种类型的属性是否存在,因此readonly
属性也可以通过别名来更改。
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'尝试
使用映射修饰符,您可以删除readonly
属性。
索引签名
有时您并不提前知道类型属性的所有名称,但您确实知道值的形状。
在这些情况下,您可以使用索引签名来描述可能值的类型,例如:
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
const secondItem: string尝试
上面,我们有一个StringArray
具有索引签名的接口。这个索引签名表明,当 aStringArray
被 a 索引时number
,它将返回 a string
。
索引签名属性类型必须是“字符串”或“数字”。
可以同时支持这两种类型的索引器...
虽然字符串索引签名是描述“字典”模式的强大方式,但它们还强制所有属性与其返回类型匹配。这是因为字符串索引声明obj.property
也可用作obj["property"]
. 在下面的例子中,name
's 的类型与字符串索引的类型不匹配,并且类型检查器给出了错误:
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;Property 'name' of type 'string' is not assignable to 'string' index type 'number'.Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}尝试
但是,如果索引签名是属性类型的联合,则可以接受不同类型的属性:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}尝试
最后,您可以制作索引签名readonly
以防止分配给它们的索引:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";Index signature in type 'ReadonlyStringArray' only permits reading.Index signature in type 'ReadonlyStringArray' only permits reading.尝试
您无法设置myArray[2]
,因为索引签名是readonly
.
扩展类型
拥有可能是其他类型的更具体版本的类型是很常见的。例如,我们可能有一个BasicAddress
类型来描述在美国发送信件和包裹所需的字段
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}尝试
在某些情况下这就足够了,但如果某个地址的建筑物有多个单元,则地址通常有一个与之关联的单元号。然后我们可以描述一个AddressWithUnit
.
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}尝试
这可以完成这项工作,但这里的缺点是我们必须重复所有其他字段,BasicAddress
因为我们的更改是纯粹添加的。相反,我们可以扩展原始BasicAddress
类型并添加唯一的新字段AddressWithUnit
。
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}尝试
an 上的extends
关键字interface
允许我们有效地从其他命名类型复制成员,并添加我们想要的任何新成员。这对于减少我们必须编写的类型声明样板的数量以及表明同一属性的几个不同声明可能相关的意图很有用。例如,AddressWithUnit
不需要重复该street
属性,并且由于street
源自BasicAddress
,读者将知道这两种类型以某种方式相关。
interface
s 也可以从多种类型扩展。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};尝试
交叉口类型
interface
s 允许我们通过扩展其他类型来构建新类型。TypeScript 提供了另一种称为交集类型的构造,主要用于组合现有的对象类型。
使用&
运算符定义交集类型。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;尝试
在这里,我们已经相交Colorful
并Circle
生成了一个新类型,其中包含Colorful
和 Circle
的所有成员。
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?尝试
接口与交叉点
我们只是研究了两种组合相似但实际上略有不同的类型的方法。使用接口,我们可以使用extends
子句从其他类型扩展,我们可以对交集做类似的事情,并用类型别名命名结果。两者之间的主要区别在于如何处理冲突,而这种区别通常是您在接口和交集类型的类型别名之间选择一个而不是另一个的主要原因之一。
通用对象类型
让我们想象一个Box
可以包含任何值的类型 - string
s、number
s、Giraffe
s 等等。
interface Box {
contents: any;
}尝试
现在,该contents
属性的类型为any
,它可以工作,但可能会导致事故发生。
我们可以改为使用unknown
,但这意味着在我们已经知道 的类型的情况下contents
,我们需要进行预防性检查,或者使用容易出错的类型断言。
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());尝试
一种类型安全的方法是Box
为每种类型的contents
.
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}尝试
但这意味着我们必须创建不同的函数或函数重载,才能对这些类型进行操作。
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}尝试
这是很多样板。此外,我们稍后可能需要引入新的类型和重载。这令人沮丧,因为我们的盒子类型和重载实际上都是相同的。
相反,我们可以创建一个声明类型参数的**泛型 类型。Box
interface Box<Type> {
contents: Type;
}尝试
您可能会将其解读为“A Box
ofType
是contents
具有类型的东西Type
”。稍后,当我们引用 时Box
,我们必须给出一个类型参数来代替Type
。
let box: Box<string>;尝试
将Box
其视为真实类型的模板,其中Type
占位符将被其他类型替换。当 TypeScript 看到时,它会将in的Box<string>
每个实例替换为,并最终使用类似. 换句话说,和我们之前的工作一模一样。Type``Box<Type>``string``{ contents: string }``Box<string>``StringBox
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
(property) Box<string>.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
(property) StringBox.contents: string尝试
Box
可重复使用,Type
可以用任何东西代替。这意味着当我们需要一个新类型的盒子时,我们根本不需要声明一个新Box
类型(尽管如果我们愿意,我们当然可以)。
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;尝试
这也意味着我们可以通过使用泛型函数来完全避免重载。
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}尝试
值得注意的是,类型别名也可以是通用的。我们可以定义我们的新Box<Type>
接口,它是:
interface Box<Type> {
contents: Type;
}尝试
通过使用类型别名来代替:
type Box<Type> = {
contents: Type;
};尝试
由于类型别名与接口不同,它可以描述的不仅仅是对象类型,我们也可以使用它们来编写其他类型的通用帮助器类型。
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
type OneOrManyOrNullStrings = OneOrMany<string> | null尝试
稍后我们将回过头来输入别名。
Array
类型_
通用对象类型通常是某种容器类型,它们独立于它们所包含的元素类型工作。数据结构以这种方式工作是理想的,这样它们就可以在不同的数据类型中重用。
事实证明,在这本手册中,我们一直在使用一种类型:Array
类型。每当我们写出number[]
or之类string[]
的类型时,这实际上只是Array<number>
and的简写Array<string>
。
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));尝试
很像Box
上面的类型,Array
它本身就是一个泛型类型。
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}尝试
现代 JavaScript 还提供了其他通用的数据结构,例如Map<K, V>
,Set<T>
和Promise<T>
. 所有这一切真正意味着,由于Map
,Set
和Promise
行为方式,它们可以与任何类型的集合一起使用。
ReadonlyArray
类型_
ReadonlyArray
是一种特殊类型,用于描述不应更改的数组。
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");Property 'push' does not exist on type 'readonly string[]'.Property 'push' does not exist on type 'readonly string[]'.
}尝试
很像readonly
属性的修饰符,它主要是我们可以用于意图的工具。当我们看到一个返回ReadonlyArray
s 的函数时,它告诉我们根本不打算更改内容,而当我们看到一个使用ReadonlyArray
s 的函数时,它告诉我们可以将任何数组传递给该函数,而不必担心它会改变它的内容。
与 不同Array
的是,没有ReadonlyArray
我们可以使用的构造函数。
new ReadonlyArray("red", "green", "blue");'ReadonlyArray' only refers to a type, but is being used as a value here.'ReadonlyArray' only refers to a type, but is being used as a value here.尝试
相反,我们可以将常规Array
s 分配给ReadonlyArray
s。
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];尝试
正如 TypeScript 为 with 提供简写语法一样,它也为Array<Type>
withType[]
提供简写ReadonlyArray<Type>
语法readonly Type[]
。
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");Property 'push' does not exist on type 'readonly string[]'.Property 'push' does not exist on type 'readonly string[]'.
}尝试
最后要注意的是,与属性修饰符不同,可分配性在常规s 和sreadonly
之间不是双向的。Array``ReadonlyArray
let x: readonly string[] = [];
let y: string[] = [];
x = y;
y = x;The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.尝试
元组类型
元组类型是另一种类型Array
,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。
type StringNumberPair = [string, number];尝试
这里,StringNumberPair
是string
和的元组类型number
。就像ReadonlyArray
,它在运行时没有表示,但对 TypeScript 很重要。对于类型系统,描述索引包含 a且索引包含 a的StringNumberPair
数组。0``string``1``number
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
doSomething(["hello", 42]);尝试
如果我们试图索引超过元素的数量,我们会得到一个错误。
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];Tuple type '[string, number]' of length '2' has no element at index '2'.Tuple type '[string, number]' of length '2' has no element at index '2'.
}尝试
我们还可以使用 JavaScript 的数组解构来解构元组。
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString);
const inputString: string
console.log(hash);
const hash: number
}尝试
元组类型在大量基于约定的 API 中很有用,其中每个元素的含义都是“显而易见的”。这使我们在解构变量时可以灵活地命名变量。在上面的例子中,我们可以命名元素
0
和1
我们想要的任何东西。但是,由于并非每个用户都对显而易见的事物持有相同的看法,因此可能值得重新考虑使用具有描述性属性名称的对象是否更适合您的 API。
除了那些长度检查之外,像这样的简单元组类型等价于Array
为特定索引声明属性的 s 版本,并length
使用数字文字类型声明。
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}尝试
您可能感兴趣的另一件事是元组可以通过写出问号(?
在元素类型之后)来具有可选属性。可选的元组元素只能出现在末尾,也会影响length
.
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
const z: number | undefined
console.log(`Provided coordinates had ${coord.length} dimensions`);
(property) length: 2 | 3
}尝试
元组也可以有剩余元素,它们必须是数组/元组类型。
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];尝试
-
StringNumberBooleans
描述一个元组,其前两个元素分别是string
和number
,但后面可以有任意数量的boolean
s. -
StringBooleansNumber
描述一个元组,它的第一个元素是string
,然后是任意数量的boolean
s 并以 a 结尾number
。 -
BooleansStringNumber
描述一个元组,其起始元素是任意数量的boolean
s 并以 a 结尾,string
然后是 anumber
。
带有剩余元素的元组没有固定的“长度”——它只有一组位于不同位置的知名元素。
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];尝试
为什么 optional 和 rest 元素可能有用?好吧,它允许 TypeScript 将元组与参数列表对应起来。元组类型可以用在rest parameters 和 arguments中,这样:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}尝试
基本上相当于:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}尝试
当你想用一个剩余参数获取可变数量的参数时,这很方便,并且你需要最少数量的元素,但你不想引入中间变量。
readonly
元组类型
关于元组类型的最后一点说明 - 元组类型有readonly
变体,可以通过readonly
在它们前面加上修饰符来指定 - 就像数组速记语法一样。
function doSomething(pair: readonly [string, number]) {
// ...
}尝试
正如您所料,readonly
TypeScript 中不允许写入元组的任何属性。
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";Cannot assign to '0' because it is a read-only property.Cannot assign to '0' because it is a read-only property.
}尝试
在大多数代码中,元组往往被创建并且未修改,因此readonly
尽可能将类型注释为元组是一个很好的默认设置。这一点也很重要,因为带有断言的数组文字将使用元组类型const
进行推断。readonly
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.尝试
在这里,distanceFromOrigin
从不修改其元素,但需要一个可变元组。由于point
’ 的类型被推断为readonly [3, 4]
,它将不兼容,[number, number]
因为该类型不能保证point
’ 的元素不会发生变异。