0
点赞
收藏
分享

微信扫一扫

【TypeScript】深入学习TypeScript对象类型


目录

  • ​​前言​​
  • ​​1、属性修改器​​
  • ​​可选属性​​
  • ​​只读属性​​
  • ​​索引签名​​
  • ​​2、扩展类型​​
  • ​​3、交叉类型​​
  • ​​4、泛型对象类型​​
  • ​​类型别名结合泛型​​
  • ​​5、数组类型​​
  • ​​6、只读数组类型​​
  • ​​7、元组类型​​
  • ​​可选的元组​​
  • ​​其余元素​​
  • ​​应用​​
  • ​​8、只读元组类型​​
  • ​​结语​​

前言

博主最近的​​TypeScript​​​文章都在​​TypeScript专栏​​里,每一篇都是博主精心打磨的精品,几乎每篇文章的质量分都达到了99,并多次入选【CSDN每天值得看】和【全站综合热榜】

订阅专栏关注博主,学习​​TypeScript​​不迷路!

好嘞,言归正传,让我们开始深入学习TypeScript对象类型吧:

在​​【TypeScript】TypeScript常用类型(上篇)​​中我们已经提到了对象类型,这篇文章将对其进行深入讲解

1、属性修改器

可选属性

在​​【TypeScript】TypeScript常用类型(上篇)​​我们已经提到了对象的可选属性,在这里我们再深入去了解一下它:

interface PaintOptions {
x?: number;
y?: number;
}

使用接口定义了一个对象类型,其中的属性都为可选属性,在​​【TypeScript】TypeScript常用类型(上篇)​​中我们已经知道不能够直接使用可选属性,需要先对其进行判空操作

function ObjFn(obj: PaintOptions) {
if (obj.x && obj.y) { // 对可选属性进行存在性判断
console.log(obj.x + obj.y);
}
}

其实这不是唯一的方式,我们也可以对可选属性设置个默认值,当该属性不存在时,使用我们设置的默认值即可,看下面这个例子:

function ObjFn({ x = 1, y = 2 }: PaintOptions) {
console.log(x + y);
}

ObjFn({ x: 4, y: 5 }); // log: 9
ObjFn({}); // log: 3

在这里,我们为 ​​ObjFn​​​的参数使用了一个解构模式,并为 ​​x​​​和 ​​y​​​提供了默认值。现在​​x​​​和 ​​y​​​ 都肯定存在于 ​​ObjFn​​​的主体中,但对于 ​​ObjFn​​的任何调用者来说是可选的。

只读属性

在​​TypeScript​​​中使用​​readonly​​修饰符可以定义只读属性:

interface NameType {
readonly name: string; // 只读属性
}
function getName(obj: NameType) {
// 可以读取 'obj.name'.
console.log(obj.name);
// 但不能重新设置值
obj.name = "Ailjx";
}

【TypeScript】深入学习TypeScript对象类型_元组


​readonly​​修饰符只能限制一个属性本身不能被重新写入,对于复杂类型的属性,其内部依旧可以改变:

interface Info {
readonly friend: string[];
readonly parent: { father: string; mother: string };
}

function getInfo(obj: Info) {
// 正常运行
obj.friend[0] = "one";
obj.parent.father = "MyFather";
// 报错
obj.friend = ["one"];
obj.parent = { father: "MyFather", mother: "MyMother" };
}

【TypeScript】深入学习TypeScript对象类型_javascript_02


​TypeScript​​在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是

否是 ​​readonly​​​ ,所以 ​​readony​​ 属性也可以通过别名来改变:

interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "AiLjx",
age: 18,
};
// 正常工作
let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // 打印 '18'
// readonlyPerson.age++; // 报错
writablePerson.age++;
console.log(readonlyPerson.age); // 打印 '19'

这里有点绕,我们来梳理一下:

  • 首先我们声明了两个几乎相同的接口类型​​Person​​ 和​​ReadonlyPerson​​ ,不同的是​​ReadonlyPerson​​ 里的属性都是只读的。
  • 之后我们定义了一个类型为​​Person​​的变量​​writablePerson​​,可知这个变量内的属性的值是可修改的。
  • 接下来有意思的是​​writablePerson​​竟然能够赋值给类型为​​ReadonlyPerson​​的变量​​readonlyPerson​​,这就验证了​​TypeScript​​在检查两个类型的属性是否兼容时,并不考虑这些类型的属性是否是 ​​readonly​​ ,所以类型为​​Person​​和​​ReadonlyPerson​​的数据可以相互赋值。
  • 此时要明白变量​​readonlyPerson​​里面的属性都是只读的,我们直接通过​​readonlyPerson.age++​​修改​​age​​是会报错的,但有意思的是我们可以通过​​writablePerson.age++​​修改​​writablePerson​​中的​​age​​,又因为对于引用类型的数据来说直接赋值就只是引用赋值(即浅拷贝),所以​​writablePerson​​变化后​​readonlyPerson​​也跟着变化了
  • 这样​​readonlyPerson​​中的只读属性就成功被修改了

对于​​TypeScript​​而言,只读属性不会在运行时改变任何行为,但在类型检查期间,一个标记为只读的属性不能被写入。

索引签名

在一些情况下,我们可能不知道对象内所有属性的名称,那属性名称都不知道,我们该怎么去定义这个对象的类型呢?

这时我们可以使用一个索引签名来描述可能的值的类型:

interface IObj {
[index: string]: string;
}
const obj0: IObj = {};
const obj1: IObj = { name: "1" };
const obj2: IObj = { name: "Ailjx", age: "18" };

  • 上面就是使用索引签名定义的一个对象类型,注意其中​​index​​是自己自定义的,代表属性名的占位,对于对象来说​​index​​的类型一般为​​string​​(因为对象的​​key​​值本身是​​string​​类型的,但也有例外的情况,往下看就知道了)
  • 最后的​​string​​就代表属性的值的类型了,从这我们不难发现使用索引签名的前提是你知道值的类型。

这时细心的朋友应该能够发现,当​​index​​​的类型为​​number​​​时,就能表示数组了,毕竟数组实质上就是一种对象,只不过它的​​key​​​其实就是数组的索引是​​number​​类型的:

interface IObj {
[index: number]: string;
}
const arr: IObj = [];
const arr1: IObj = ["Ailjx"];
const obj: IObj = {}; // 赋值空对象也不会报错
const obj1: IObj = { 1: "1" }; // 赋值key为数字的对象也不会报错

  • ​index: number​​时不仅能够表示数组,也能够表示上面所示的两种对象,这就是上面提到的例外的情况。

这是因为当用 "数字 “进行索引时,​​JavaScript​​实际上会在索引到一个对象之前将其转换为 “字符串”。这意味着用1 (一个数字)进行索引和用"1” (一个字符串)进行索引是一样的,所以两者需要一致。

索引签名的属性类型必须是 ​​string​​​ 或 ​​number​​ ,称之为数字索引器字符串索引器,支持两种类型的索引器是可能的,但是从数字索引器返回的类型必须是字符串索引器返回的类型的子类型(这一点特别重要!),如:

interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}

interface IObj {
[index: number]: Dog;
[index: string]: Animal;
}

  • 在​​【TypeScript】TypeScript常用类型(下篇)​​中我们已经讲解过接口类型的扩展,这里就不再多说了,从上面的代码中可以知道的是​​Dog​​ 是​​Animal​​ 的子类,所以上述代码是可选的,如果换一下顺序就不行了:
  • 【TypeScript】深入学习TypeScript对象类型_元组_03

字符串索引签名强制要求所有的属性与它的返回类型相匹配

  • 在下面的例子中,​​name​​ 的类型与字符串索引的类型不匹配,类型检查器会给出一个错误:
  • 【TypeScript】深入学习TypeScript对象类型_学习_04

数字索引签名没有该限制

然而,如果索引签名是属性类型的联合,不同类型的属性是可以接受的:

interface IObj {
[index: string]: number | string;
length: number; // ok
name: string; // ok
}

索引签名也可以设置为只读:

【TypeScript】深入学习TypeScript对象类型_javascript_05

2、扩展类型

在​​【TypeScript】TypeScript常用类型(下篇)​​接口中我们简单介绍过扩展类型,在这里再详细讲一下:

interface User {
name: string;
age: number;
}
interface Admin {
isAdmin: true;
name: string;
age: number;
}

这里声明了两个类型接口,但仔细发现它们其实是相关的(​​Admin​​​是​​User​​​的一种),并且它们之间重复了一些属性,这时就可以使用​​extends​​扩展:

interface User {
name: string;
age: number;
}
interface Admin extends User {
isAdmin: true;
}

接口上的 ​​extends​​ 关键字,允许我们有效地从其他命名的类型中复制成员,并添加我们想要的任何新成员。

这对于减少我们不得不写的类型声明模板,以及表明同一属性的几个不同声明可能是相关的意图来说,是非常有用的。例如, ​​Admin​​​不需要重复 ​​name​​​和​​age​​​属性,而且因为 ​​name​​​和​​age​​​源于​​User​​,我们会知道这两种类型在某种程度上是相关的。

接口也可以从多个类型中扩展:

interface User {
name: string;
}
interface Age {
age: number;
}
interface Admin extends User, Age {
isAdmin: true;
}

多个父类使用​​,​​分割

3、交叉类型

在​​【TypeScript】TypeScript常用类型(下篇)​​类型别名中我们已经介绍过交叉类型​​&​​,这里就不再过多的说了:

interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;

const cc: ColorfulCircle = {
color: "red",
radius: 42,
};

4、泛型对象类型

如果我们有一个盒子类型,它的内容可以为字符串,数字,布尔值,数组,对象等等等等,那我们去定义它呢?这样吗:

interface Box {
contents: any;
}

现在,内容属性的类型是任意,这很有效,但我们知道​​any​​​会导致​​TypeScript​​失去编译时的类型检查,这显然是不妥的

我们可以使用 ​​unknown​​ ,但这意味着在我们已经知道内容类型的情况下,我们需要做预防性检查,或者使用容易出错的类型断言

interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// 我们需要检查 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// 或者用类型断言
console.log((x.contents as string).toLowerCase());

这显得复杂了一些,并且也不能保证​​TypeScript​​​能够追踪到​​contents​​具体的类型

针对这种需求,我们就可以使用泛型对象类型,做一个通用的​Box​​ 类型,声明一个类型参数:

// 这里的Type是自定义的
interface Box<Type> {
contents: Type;
}

当我们引用 ​​Box​​ 时,我们必须给一个类型参数来代替 ​​Type​​:

const str: Box<string> = {
contents: "999", // contents类型为string
}; // str类型等价于{ contents:string }
const str1: Box<number> = {
contents: 1, // contents类型为number
}; // str1类型等价于{ contents:number }

【TypeScript】深入学习TypeScript对象类型_javascript_06

这像不像是函数传参的形式?其实我们完全可以将​​Type​​​理解为形参,在使用类型时通过泛型语法​​<>​​传入实参即可

这样我们不就实现了我们想要的效果了吗,​​contents​​​的类型可以是我们指定的任意的类型,并且​​TypeScript​​可以追踪到它具体的类型。

  • 复杂一点的应用:使用泛型对象类型实现通用函数

interface Box<Type> {
contents: Type;
}
function setContents<FnType>(box: Box<FnType>, newContents: FnType): FnType {
box.contents = newContents;
return box.contents;
}

const a: string = setContents<string>({ contents: "Ailjx" }, "9");
console.log(a); // '9'
const b: number = setContents({ contents: 2 }, 2);
console.log(b); // 2
const c: boolean = setContents({ contents: true }, false);
console.log(c); // false

这里在函数身上使用了泛型,定义了类型参数​​FnType​​​ :​​setContents<FnType>​​​,之后函数的参数​​box​​​的类型为​​Box<FnType>​​​(将接收到的参数传递给​​Box​​​),​​newContents​​​的类型为​​FnType​​​,函数返回值也是​​FnType​​类型

观察常量​​a​​​,它调用​​setContents​​​函数时传入了​​string​​​,​​string​​​就会替换掉​​setContents​​​函数中的所有​​FnType​​​,则函数的两个参数的类型就是​​{conents:string}​​​和​​string​​​,函数返回值也是​​string​​类型

其实这里调用​​setContents​​​函数时我们可以不去手动传递类型参数,​​TypeScript​​​会非常聪明的根据我们调用函数传入的参数类型推断出​​FnType​​​是什么,就像常量​​b​​​和​​c​​的使用一样

类型别名结合泛型

类型别名也可以是通用的,我们完全可以使用类型别名重新定义 ​​Box<Type>​​:

type Box<Type> = {
contents: Type;
};

由于类型别名与接口不同,它不仅可以描述对象类型,我们还可以用它来编写其他类型的通用辅助类型:

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

上面的例子中嵌套使用了类型别名,多思考一下不难看懂的

通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种方式工作是很理想的,这样它们就可以在不同的数据类型中重复使用。

5、数组类型

和上面的 ​​Box​​​ 类型一样, ​​Array​​​ 本身也是一个通用类型, ​​number[]​​​ 或 ​​string[] 这​​​实际上只是 ​​Array<number>​​​ 和​​Array<string>​​ 的缩写。

​Array​​泛型对象的部分源码:

interface Array<Type> {
/**
* 获取或设置数组的长度。
*/
length: number;
/**
* 移除数组中的最后一个元素并返回。
*/
pop(): Type | undefined;
/**
* 向一个数组添加新元素,并返回数组的新长度。
*/
push(...items: Type[]): number;

// ...
}

现代​​JavaScript​​​还提供了其他通用的数据结构,比如 ​​Map<K, V>​​​ , ​​Set<T>​​​ , 和 ​​Promise<T>​​​ 。这实际上意味着,由于​​Map​​​ 、​​Set​​​ 和​​Promise​​ 的行为方式,它们可以与任何类型的集合一起工作。

6、只读数组类型

​ReadonlyArray​​ 是一个特殊的类型,描述了不应该被改变的数组。

function doStuff(values: ReadonlyArray<string>) {
// 我们可以从 'values' 读数据...
const copy = values.slice();
console.log(`第一个值是 ${values[0]}`);
// ...但我们不能改变 'vulues' 的值。
values.push("hello!");
values[0] = "999";
}

【TypeScript】深入学习TypeScript对象类型_typescript_07

  • ​ReadonlyArray<string>​​​与普通数组一样也能够简写,可简写为:​​readonly string[]​
  • 普通的​​Array​​ 可以分配给 ​​ReadonlyArray​​ :

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

而 ​​ReadonlyArray​​​ 不能分配给普通 ​​Array​​ :

【TypeScript】深入学习TypeScript对象类型_javascript_08

7、元组类型

​Tuple​​​ 类型是另一种 ​​Array​​ 类型,它确切地知道包含多少个元素,以及它在特定位置包含哪些类型。

type MyType = [number, string];
const arr: MyType = [1, "1"];

这里的​​MyType​​​就是一个元组类型,对于类型系统来说, ​​MyType​​​描述了其索
引 0 包含数字和 索引1 包含字符串的数组,当类型不匹配时就会抛出错误:

【TypeScript】深入学习TypeScript对象类型_数组_09

当我们试图索引超过元素的数量,我们会得到一个错误:

【TypeScript】深入学习TypeScript对象类型_数组_10

需要注意的是

  • 这里我们虽然只声明了数组的前两个元素的类型,但这不代表数组内只能有两个元素
  • 我们依旧可以向其​​push​​新元素,但新元素的类型必须是我们声明过的类型之一
  • 并且添加新元素后虽然数组的长度变化了,但我们依旧无法通过索引访问新加入的元素(能访问到的索引依旧不超过先前类型定义时的元素数量)

const arr: MyType = [1, "1"];
arr.push(3);
arr.push("3");
console.log(arr, arr.length); // [ 1, '1', 3, '3' ] 4
console.log(arr[0], arr[1]); // 1 '1'
// console.log(arr[2]); // err:长度为 "2" 的元组类型 "MyType" 在索引 "2" 处没有元素。
// arr.push(true); // err:类型“boolean”的参数不能赋给类型“string | number”的参数

对元组进行解构:

function fn(a: [string, number]) {
const [str, num] = a;
console.log(str); // type str=string
console.log(num); // type num=number
}

  • 这里需要注意的是我们解构出的数据是一个常数,不能被修改:

function fn(a: [string, number]) {
const [str, num] = a;
console.log(a[1]++); // ok
console.log(num++); // err:无法分配到 "num" ,因为它是常数
}

可选的元组

元组可以通过在元素的类型后面加上​​?​​使其变成可选的,它只能出现在数组末尾,而且还能影响到数组长度。

type MyArr = [number, number, number?];
function getLength(arr: MyArr) {
const [x, y, z] = arr; // z的类型为number|undefined
console.log(`数组长度:${arr.length} `);
}
getLength([3, 4]); //数组长度:2
getLength([3, 4, 5]); // 数组长度:3
getLength([3, 4, "5"]); // err:不能将类型“string”分配给类型“number”。

其余元素

元组也可以有其余元素,这些元素必须是 ​​array/tuple​​ 类型:

type Arr1 = [string, number, ...boolean[]];
type Arr2 = [string, ...boolean[], number];
type Arr3 = [...boolean[], string, number];
const a: Arr1 = ["Ailjx", 3, true, false, true, false, true];

  • ​Arr1​​描述了一个元组,其前两个元素分别是字符串和数字,但后面可以有任意数量的布尔。
  • ​Arr2​​描述了一个元组,其第一个元素是字符串,然后是任意数量的布尔运算,最后是一个数字。
  • ​Arr3​​描述了一个元组,其起始元素是任意数量的布尔运算,最后是一个字符串,然后是一个数字。

应用

function fn(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
console.log(name, version, input); // 1 1 [ true, false ]
console.log('参数数量:',args.length); // 参数数量:4
// ...
}
fn("1", 1, true, false);

几乎等同于:

function fn(name: string, version: number, ...input: boolean[]) {
console.log(name, version, input); // 1 1 [ true, false ]
console.log(input.length + 2); // 参数数量:4
// ...
}
fn("1", 1, true, false);

8、只读元组类型

​tuple​​​ 类型有只读特性,可以通过在它们前面加上一个​​readonly​​修饰符来指定:

let arr: readonly [string, number] = ["1", 1];
arr[0] = "9"; // err:无法分配到 "0" ,因为它是只读属性。

在大多数代码中,元组往往被创建并不被修改,所以在可能的情况下,将类型注释为只读元组是一个很好的默认。

带有 ​​const​​ 断言的数组字面量将被推断为只读元组类型,且元素的类型为文字类型:

【TypeScript】深入学习TypeScript对象类型_typescript_11


与只读数组类型中一样,普通的元组可以赋值给只读的元组,但反过来不行:

let readonlyArr: readonly [number, number];
let arr1: [number, number] = [5, 5];
readonlyArr = arr1; // ok
let arr2: [number, number] = readonlyArr; // err

结语

上篇文章​​【TypeScript】TypeScript中类型缩小(含类型保护)与类型谓词​​入选了《全站综合热榜》,感谢CSDN和各位支持我的朋友,我会继续努力,保持状态创作出更多更好的文章!


举报

相关推荐

0 条评论