<Type>Level TypeScript-3. Objects & Records
Objects
和Records
是我们可以在TypeScript中操作的两种最常见的数据结构。它们将对我们的编写TypeScript起到至关重要的作用。我知道大家肯定已经使用过Objects
和Records
,但我还是希望通过本章提升大家对它们所代表的值的理解,并向大家展示它们是如何运作的。
type SomeObject = { key1: boolean; key2: number };
type SomeRecord = { [key: string]: number };
在我们开始之前,我要提醒大家我们在上一章中提到的一些重要概念,因为它们也将与本章息息相关。在深入探讨类型与集合之间的关系之前,我们简要地介绍了五种主要类型——原始类型、字面量类型、数据结构类型、联合类型和交叉类型。
我们之前经过讨论得出结论:类型代表一组值的集合,就像集合一样,类型也可以包括其他类型,我们称这种关系为子类型。
最后,我们认识了两种特殊的类型:never
-空集,和unknown
-包含其他一切的集合。
现在,让我们关注上一章中提到的四种数据结构类型中的两种:Objects
和Records
:
type FourKindsOfDataStructures =
| { key1: boolean; key2: number } // objects
| { [key: string]: number } // records
| [boolean, number] // tuples
| number[]; // arrays
Object types
Object
类型定义JavaScript对象集合。创建对象类型的语法与我们创建常规对象的方式非常相似:
type User = {
name: string;
age: number;
role: "admin" | "standard";
};
事实上,Objects
是JS对象的类型级等价物。就像JS对象一样,它们可以包含我们想要的任意多个属性,并且每个属性都由一个唯一的键索引。请注意,每个键可以包含不同的类型:name
键保存类型string
的值,而age
键保存类型number
的值。
我们创建的User
类型是包含name
、age
和role
属性的所有对象的集合:
// ✅ this object is in the `User` set.
const gabriel: User = {
name: "Gabriel",
role: "admin",
age: 28,
};
// ❌
const bob: User = {
name: "Bob",
age: 45,
// <- the `role` key is missing.
};
// ❌
const peter: User = {
name: "Peter",
role: "standard",
age: "45" /* <- the `age` key should be of type `number`,
but it's assigned to a `string`. */,
};
上面展示了属性缺失,类型不匹配等情况,那拥有额外属性的对象能否分配给User
类型呢?
答案是:是… 也不是… , 让我们继续往下看!
对象的可分配性
-
场景1—显示定义
如果您使用显式类型注释将对象赋给变量,TypeScript将拒绝额外的属性:
const alice: User = { name: "Alice", age: 35, role: "admin", bio: "...", /* ~~~~~~~~~ ^ ❌ This doesn't type-check. */ };
你将得到👇🏻下面报错:
Object literal may only specify known properties, and 'bio' does not exist in type 'User'. (2322)
术语
Object literal
是指使用{}语法内联定义的对象。 -
场景2—赋值
那如果我们将一个预先存在的具有额外属性的对象分配给
alice
会怎么样?const looksLikeAUser = { name: "Alice", age: 35, role: "admin" as const, bio: "...", // <- extra prop! }; // ✅ This works just fine! const alice: User = looksLikeAUser;
你会发现,上述代码 并不会报错!这是为什么呢?
对于拥有额外属性的对象能否分配给类型User
类型 这个问题,明确的答案是,具有额外属性的对象可以分配给具有较少属性的对象,但在对象是显示定义的场景中,TypeScript有额外的规则来确保我们不会错误地分配后面无法使用的属性,因为我们的类型会禁止我们这样做。
这意味着你绝对不能保证某种类型的对象不包含额外的属性!对象类型是至少具有它定义的所有属性的对象集。
读取属性
要访问属性的类型,我们可以使用方括号表示法:
type User = { name: string; age: number; role: "admin" | "standard" };
type Age = User["age"]; // => number
type Role = User["role"]; // => "admin" | "standard"
.
点符号是不起作用的!
type Age = User.age;
// ^ ❌ syntax error!
一次读取多个属性
您可能已经注意到,在User[“age”]
等表达式中,键“age”
是一种字面量类型。如果你试图通过一个字面量类型的联合类型替换“age”
,会发生什么呢?
type User = { name: string; age: number; role: "admin" | "standard" };
type NameOrAge = User["name" | "age"]; // => string | number
可以看到,这种方式是行得通的!这就好像我们同时访问“name”
和“age”
,并获取它们所包含的类型的联合类型。从一个对象访问多个键相当于分别访问每个键,然后用它们的结果构造一个联合类型:
type NameOrAge = User["name"] | User["age"]; // => string | number
keyof 关键字
keyof
关键字允许检索对象类型中所有键的并集。您可以将其放置在任何对象之前:
type User = {
name: string;
age: number;
role: "admin" | "standard";
};
type Keys = keyof User; // "name" | "age" | "role"
由于keyof
返回对象属性的并集,因此我们可以将其与方括号表示法组合,以检索此对象中所有值类型的并集!
type User = {
name: string;
age: number;
role: "admin" | "standard";
};
type UserValues = User[keyof User]; // string | number | "admin" | "standard"
这是一个非常常见的用例,我们通常会定义ValueOf
泛型类型来对该模式进行抽象:
type ValueOf<Obj> = Obj[keyof Obj];
type UserValues = ValueOf<User>; // string | number | "admin" | "standard"
泛型是TypeScript类型世界中的函数。现在,我们可以对任何对象类型重用此逻辑了!🎉
?可选属性
对象类型可以定义可能存在或不存在的属性。将属性设置为可选的方法是使用?
修饰属性
type BlogPost = { title: string; tags?: string[] };
// ^ this property is optional!
// ✅ No `tags` property
const blogBost1: BlogPost = { title: "introduction" };
// ✅ `tags` contains a list of strings
const blogBost2: BlogPost = {
title: "part 1",
tags: ["#easy", "#beginner-friendly"],
};
有人会提出疑问? 为什么不使用下面这种方式定义可选属性呢?
type BlogPost = { title: string; tags: string[] | undefined };
这是因为对象必须为其类型上存在的所有属性定义值。TypeScript会要求我们将tags
属性显式分配给undefined
:
const blogBost1: BlogPost = { title: "part 1" };
// ^ ❌ type error: the `tags` key is missing.
// ✅
const blogBost2: BlogPost = { title: "part 1", tags: undefined };
为所有可选的属性分配undefined
是不方便的。最好有一种方法告诉TypeScript,某些属性是可以省略的!
& 使用交叉类型合并对象类型
为了使我们的代码更模块化,有时将类型定义拆分为多个对象类型很有用。让我们将User
类型分为三部分:
type WithName = { name: string };
type WithAge = { age: number };
type WithRole = { role: "admin" | "standard" };
现在我们需要一种方法将它们重新组装成一种类型。我们可以使用交叉类型:
type User = WithName & WithAge & WithRole;
type Organization = WithName & WithAge; // organizations don't have a role
交叉类型创建出来的User
类型与我们之前定义的User
类型,拥有完全相同的属性。
对象的交集和属性的并集
不知道大家有没有这种一问?{a:string}
和{b:number}
的交集为什么是{a:string,b:number}
🤔?,为什么不是never
?交叉的结果不应该是缩小集合范围吗?
先提前告诉大家答案, 原因如下:我们不是在交叉它们的键,而是在交叉它们所代表的值的集合。
由于具有额外键的对象类型可分配给具有较少键的对象,因此在集合{a:string}
中存在集合{a:string,b:number}
。集合{b:number}
中同样存在集合{a:string,b:number}
。
如果上面的结论你理解起来比较困难,你也可以按照下面的思路去理解:
对于对象类型 objectA
而言,objectA
拥有的属性越多,则意味着objectA
对值的约束条件越多,符合条件可分配给objectA
的值越少,objectA
所代表的集合范围越小。
因此,将{a:string}
与{b:number}
相交将返回同时属于这两个集合的一组值,表示为{a:string,b:number}
。
事实证明,两个对象的交集包含其键的并集:
type A = { a: string }; // 只拥有属性a
type KeyOfA = keyof A; // => 'a'
type B = { b: number }; // 只拥有属性b
type KeyOfB = keyof B; // => 'b'
type C = A & B;
type KeyOfC = keyof C; // => 'a' | 'b'
相反,两个对象的并集包含其关键点的交集:
type A = { a: string; c: boolean };
type KeyOfA = keyof A; // => 'a' | 'c'
type B = { b: number; c: boolean };
type KeyOfB = keyof B; // => 'b' | 'c'
type C = A | B;
type KeyOfC = keyof C; // => ('a' | 'c') & ('b' | 'c') <=> 'c'
通过上述两个结论,可以得出两个公式:
keyof (A & B) = (keyof A) | (keyof B)
keyof (A | B) = (keyof A) & (keyof B)
对象交叉是递归进行的
type WithName = { name: string; id: string };
type WithAge = { age: number; id: number };
type User = WithName & WithAge;
type Id = User["id"]; // => string & number <=> never
Records
与对象类型一样,record
也表示对象集。不同之处在于,record
的所有键必须共享相同的类型。
type RecordOfBooleans = { [key: string]: boolean };
还可以使用内置的“Record
”泛型来定义record
:
type RecordOfBooleans = Record<string, boolean>;
其定义如下:
type Record<K, V> = { [Key in K]: V };
注意in
关键字。这使用了一个叫做Mapped Types
的特性,我们将在一个专门的章节中详细介绍。简言之,in
让我们为联合类型K
中的每个键分配一种类型的值。
在前面的示例中,我们将类型string
传递为K
,但我们也可以使用字面量类型的联合:
type InputState = Record<"valid" | "edited" | "focused", boolean>;
type InputState = { valid: boolean; edited: boolean; focused: boolean };
这等同于👇🏻
type InputState = { valid: boolean; edited: boolean; focused: boolean };
Helper Functions
TypeScript提供了几个内置的帮助函数来处理非常有用的对象类型。我们将很快学习如何创建自己的函数,使用Mapped Types
以各种方式转换对象,但现在,让我们看看内置可用的函数。
Partial
Partial
泛型接受一个对象类型,并返回另一个相同的对象类型,但其所有属性都是可选的:
type Props = { value: string; focused: boolean; edited: boolean };
type PartialProps = Partial<Props>;
// is equivalent to:
type PartialProps = { value?: string; focused?: boolean; edited?: boolean };
Required
Required
泛型与Partial
相反。它接受一个对象并返回另一个相同的对象,只是它的所有属性都是必需的:
type Props = { value?: string; focused?: boolean; edited?: boolean };
type RequiredProps = Required<Props>;
// is equivalent to:
type RequiredProps = { value: string; focused: boolean; edited: boolean };
Pick
Pick
泛型用于从对象类型中提取某些属性,组成新的对象类型
type Props = { value: string; focused: boolean; edited: boolean };
type ValueProps = Pick<Props, "value">;
// is equivalent to:
type ValueProps = { value: string };
type SomeProps = Pick<Props, "value" | "focused">;
// is equivalent to:
type SomeProps = { value: string; focused: boolean };
Omit
Omit
与Pick
相反,用于从对象类型中排除某些属性,并通过剩余属性组成的新的对象类型
type Props = { value: string; focused: boolean; edited: boolean };
type ValueProps = Omit<Props, "value">;
// is equivalent to:
type ValueProps = { edited: boolean; focused: boolean };
type OtherProps = Omit<Props, "value" | "focused">;
// is equivalent to:
type OtherProps = { edited: boolean };
挑战
/**
* 1. implement a generic to get the union of all keys of an object type.
*/
namespace keys {
type Keys<Obj> = TODO
type res1 = Keys<{ a: number; b: string }>;
type test1 = Expect<Equal<res1, "a" | "b">>;
type res2 = Keys<{ a: number; b: string; c: unknown }>;
type test2 = Expect<Equal<res2, "a" | "b" | "c">>;
type res3 = Keys<{}>;
type test3 = Expect<Equal<res3, never>>;
type res4 = Keys<{ [K in string]: boolean }>;
type test4 = Expect<Equal<res4, string>>;
}
/**
* 2. implement a generic to get the union of all values in an object type.
*/
namespace valueof {
type ValueOf<Obj> = TODO
type res1 = ValueOf<{ a: number; b: string }>;
type test1 = Expect<Equal<res1, number | string>>;
type res2 = ValueOf<{ a: number; b: string; c: boolean }>;
type test2 = Expect<Equal<res2, number | string | boolean>>;
type res3 = ValueOf<{}>;
type test3 = Expect<Equal<res3, never>>;
type res4 = ValueOf<{ [K in string]: boolean }>;
type test4 = Expect<Equal<res4, boolean>>;
}
/**
* 3. Create a generic that removes the `id` key
* from an object type.
*/
namespace removeId {
type RemoveId<Obj> = TODO
type res1 = RemoveId<{
id: number;
name: string;
age: unknown;
}>;
type test1 = Expect<
Equal<res1, { name: string; age: unknown }>
>;
type res2 = RemoveId<{
id: number;
title: string;
content: string;
}>;
type test2 = Expect<
Equal<res2, { title: string; content: string }>
>;
}
/**
* 4. combine Partial, Omit and Pick to create a generic
* that makes the `id` key of an object type optional.
*/
namespace optionalId {
/** This is called a type constraint.
* We'll learn more about them soon.
* 👇 */
type MakeIdOptional<Obj extends { id: unknown }> =
TODO
type res1 = MakeIdOptional<{
id: number;
name: string;
age: unknown;
}>;
type test1 = Expect<
Equal<res1, { id?: number } & { name: string; age: unknown }>
>;
type res2 = MakeIdOptional<{
id: string;
title: string;
content: string;
}>;
type test2 = Expect<
Equal<res2, { id?: string } & { title: string; content: string }>
>;
}
/**
* 5. Since intersections are applied recursively,
* how would you write an `Assign<A, B>` type-level
* function that matches the behavior of `{...a, ...b}`,
* and overrides properties of `A` with properties of `B`?
*/
namespace assign {
type Assign<A, B> = TODO
const assign = <A, B>(obj1: A, obj2: B): Assign<A, B> => ({
...obj1,
...obj2,
});
// Override `id`
type res1 = Assign<{ name: string; id: number }, { id: string }>;
type test1 = Expect<Equal<res1, { name: string } & { id: string }>>;
// Override `age` and `role`
type res2 = Assign<
{ name: string; age: string; role: string },
{ age: 42; role: "admin" }
>;
type test2 = Expect<
Equal<res2, { name: string } & { age: 42; role: "admin" }>
>;
// No overlap
type res3 = Assign<{ name: string; id: number }, { age: number }>;
type test3 = Expect<
Equal<res3, { name: string; id: number } & { age: number }>
>;
// Using type inference from values
const res4 = assign({ name: "Bob", id: 4 }, { id: "3" });
type test4 = Expect<Equal<typeof res4, { name: string } & { id: string }>>;
}
转载自:https://juejin.cn/post/7207252621685063740