likes
comments
collection
share

<Type>Level TypeScript-3. Objects & Records

作者站长头像
站长
· 阅读数 9

ObjectsRecords是我们可以在TypeScript中操作的两种最常见的数据结构。它们将对我们的编写TypeScript起到至关重要的作用。我知道大家肯定已经使用过ObjectsRecords,但我还是希望通过本章提升大家对它们所代表的值的理解,并向大家展示它们是如何运作的。

type SomeObject = { key1: boolean; key2: number };

type SomeRecord = { [key: string]: number };

在我们开始之前,我要提醒大家我们在上一章中提到的一些重要概念,因为它们也将与本章息息相关。在深入探讨类型与集合之间的关系之前,我们简要地介绍了五种主要类型——原始类型、字面量类型、数据结构类型、联合类型和交叉类型

我们之前经过讨论得出结论:类型代表一组值的集合,就像集合一样,类型也可以包括其他类型,我们称这种关系为子类型

<Type>Level TypeScript-3. Objects & Records

最后,我们认识了两种特殊的类型:never-空集,和unknown-包含其他一切的集合。

现在,让我们关注上一章中提到的四种数据结构类型中的两种:ObjectsRecords

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类型是包含nameagerole属性的所有对象的集合:

// ✅ 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. 场景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. 场景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>Level TypeScript-3. Objects & Records

这意味着你绝对不能保证某种类型的对象不包含额外的属性!对象类型是至少具有它定义的所有属性的对象集。

读取属性

要访问属性的类型,我们可以使用方括号表示法:

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>Level TypeScript-3. Objects & Records

事实证明,两个对象的交集包含其键的并集

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

OmitPick相反,用于从对象类型中排除某些属性,并通过剩余属性组成的新的对象类型

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
评论
请登录