鸿蒙harmony--TypeScript对象详解
生活就是用那一两分的甜,冲淡那八九分的苦,把身体照顾好,把喜欢的事做好,把重要的人待好,你要的一切都在路上!
目录
一,定义
二,属性修饰符
2.1 可选属性
2.2 readonly 属性
三,扩展类型
四,交叉类型
五,泛型对象类型
5.1 Array类型
5.2 ReadonlyArray 类型
5.3 元组类型
5.4 readonly 元组类型
一,定义
在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型来表示它们。
方式一:匿名
function yz(yuanZhen:{name:string,age:number}){
return yuanZhen.name+yuanZhen.age
}
export default class HanShu{
test(){
console.log("s="+yz({name:"袁震",age:30}))
}
}
方式二:接口
interface YuanZhen{
name:string
age:number
}
function yz1(yuanZhen:YuanZhen){
return yuanZhen.name+yuanZhen.age
}
export default class HanShu{
test(){
console.log("s="+yz1({name:"袁震",age:30}))
}
}
方式三:类型别名
type YuanZhen2={
name:string,
age:number
}
function yz2(yuanZhen:YuanZhen2){
return yuanZhen.name+yuanZhen.age
}
export default class HanShu{
test(){
console.log("s="+yz2({name:"袁震",age:30}))
}
}
在上述所有三个示例中,我们编写的函数接受包含属性 name
(必须是 string
)和 age
(必须是 number
)的对象。
二,属性修饰符
对象类型中的每个属性都可以指定一些内容:类型,属性是否可选,属性是否可以写入。
2.1 可选属性
很多时候,我们会发现自己在处理可能具有属性集的对象。在这些情况下,我们可以通过在其名称末尾添加问号 (?
) 来将这些属性标记为可选。
interface YuanZhen3{
name:string
age?:number
address?:string
}
function yz3(yuanZhen:YuanZhen3){
return yuanZhen.name+yuanZhen.age
}
export default class HanShu{
test(){
console.log("s="+yz2({name:"袁震",age:30}))
yz3({name:"袁震"})
yz3({name:"袁震",age:30})
yz3({name:"袁震",age:30,address:"淄博"})
}
}
在这个例子中,age
和 address
都被认为是可选的。我们可以选择提供其中任何一个,因此上面对 yz3
的每个调用都是有效的。所有的可选性真正说明的是,如果设置了属性,它最好有一个特定的类型。
2.2 readonly
属性
对于 TypeScript,属性也可以标记为 readonly
。虽然它不会在运行时改变任何行为,但在类型检查期间无法写入标记为 readonly
的属性。
interface YuanZhen4{
name:string
readonly age:number
}
function yz4(yuanZhen:YuanZhen4){
console.info(yuanZhen.name+yuanZhen.age)
//Cannot assign to 'age' because it is a read-only property. <ArkTSCheck>
yuanZhen.age=30
}
readonly 标记的属性 只读不可写
使用 readonly
修饰符并不一定意味着值是完全不可变的 - 或者换句话说,其内部内容无法更改。这只是意味着属性本身不能被重写。
interface YuanZhen5{
readonly a:{name:string,age:number}
}
function yz5(yuanZhen:YuanZhen5){
//ok
console.info(yuanZhen.a.name+yuanZhen.a.age)
yuanZhen.a.name ="袁震"
yuanZhen.a.age=30
}
function yz55(yuanZhen:YuanZhen5){
//报错:Cannot assign to 'a' because it is a read-only property. <ArkTSCheck>
yuanZhen.a ={
name:"袁震",
age:30
}
}
管理对 readonly
含义的期望很重要。在 TypeScript 的开发期间触发关于如何使用对象的意图很有用。TypeScript 在检查两种类型是否兼容时不会考虑这两种类型的属性是否为 readonly
,因此 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'
三,扩展类型
拥有可能是其他类型的更具体版本的类型是很常见的。例如,我们可能有一个 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;
}
interface
上的 extends
关键字允许我们有效地从其他命名类型复制成员,并添加我们想要的任何新成员。这对于减少我们必须编写的类型声明样板的数量以及表明同一属性的几个不同声明可能相关的意图很有用。例如,AddressWithUnit
不需要重复 street
属性,因为 street
源自 BasicAddress
,所以读者会知道这两种类型在某种程度上是相关的。
interface
还可以从多种类型扩展:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
四,交叉类型
interface
允许我们通过扩展其他类型来构建新类型。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 });
//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'?
draw({ color: "red", raidus: 42 });
我们只是研究了两种组合相似但实际上略有不同的类型的方法。使用接口,我们可以使用 extends
子句从其他类型扩展,我们可以对交叉做类似的事情,并用类型别名命名结果。两者之间的主要区别在于冲突的处理方式,这种区别通常是你在接口和交叉类型的类型别名之间选择一个而不是另一个的主要原因之一。
五,泛型对象类型
让我们想象一个可以包含任何值的 Box
类型 - string
、number
、Giraffe
,等等。
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());
一种类型安全的方法是为每种类型的 contents
搭建不同的 Box
类型。
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;
}
你可能会将其理解为“Type
的 Box
是其 contents
具有类型 Type
的东西”。稍后,当我们引用 Box
时,我们必须给出一个类型参数来代替 Type
。
let box: Box<string>;
将 Box
视为真值类型的模板,其中 Type
是一个占位符,将被其他类型替换。当 TypeScript 看到 Box<string>
时,它会将 Box<Type>
中的每个 Type
实例替换为 string
,并最终使用 { contents: string }
之类的东西。换言之,Box<string>
和我们之前的 StringBox
工作方式相同。
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
5.1 Array类型
泛型对象类型通常是某种容器类型,它们独立于它们所包含的元素类型工作。数据结构以这种方式工作是理想的,这样它们就可以在不同的数据类型中重用。
我们一直在使用一种类型:Array
型。每当我们写出像 number[]
或 string[]
这样的类型时,这实际上只是 Array<number>
和 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;
// ...
}
5.2 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[]'.
}
就像属性的 readonly
修饰符一样,它主要是我们可以用于意图的工具。当我们看到一个返回 ReadonlyArray
的函数时,它告诉我们根本不打算更改内容,而当我们看到一个消耗 ReadonlyArray
的函数时,它告诉我们可以将任何数组传递到该函数中,而不必担心它会更改其内容。
与 Array
不同,我们没有可以使用的 ReadonlyArray
构造函数。相反,我们可以将常规 Array
分配给 ReadonlyArray
。
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
正如 TypeScript 为 Array<Type>
和 Type[]
提供简写语法一样,它也为 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[]'.
}
最后要注意的一点是,与 readonly
属性修饰符不同,可赋值性在常规 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[]'.
5.3 元组类型
元组类型是另一种 Array
类型,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。
type StringNumberPair = [string, number];
这里,StringNumberPair
是 string
和 number
的元组类型。与 ReadonlyArray
一样,它在运行时没有表示,但对 TypeScript 很重要。对于类型系统,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'.
}
我们也可以使用 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
版本的类型,以及使用数字字面量类型声明 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
。
StringBooleansNumber
描述一个元组,其第一个元素是 string
,然后是任意数量的 boolean
,最后以 number
结尾。
BooleansStringNumber
描述了一个元组,其起始元素是任意数量的 boolean
,并以 string
和 number
结尾。
具有剩余元素的元组没有设置 “length” - 它只有一组不同位置的众所周知的元素。
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
为什么可选的和剩余的元素可能有用?好吧,它允许 TypeScript 将元组与参数列表对应起来。元组类型可以在剩余形参和实参中使用,因此如下:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
基本上相当于:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
当你想用一个剩余参数获取可变数量的参数时,这很方便,并且你需要最少数量的元素,但你不想引入中间变量。
5.4 readonly
元组类型
关于元组类型的最后一点说明 - 元组类型有 readonly
变体,可以通过在它们前面添加 readonly
修饰符来指定 - 就像数组简写语法一样。
function doSomething(pair: readonly [string, number]) {
// ...
}
正如你所料,TypeScript 中不允许写入 readonly
元组的任何属性。
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
//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]'.
在这里,distanceFromOrigin
从不修改其元素,但需要一个可变元组。由于 point
的类型被推断为 readonly [3, 4]
,它不会与 [number, number]
兼容,因为该类型不能保证 point
的元素不会发生修改。