likes
comments
collection
share

<Type>Level TypeScript-2.类型只是数据

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

每种编程语言都是关于转换数据的,类型级别的TypeScript也不例外。与其他编程语言的主要区别在于类型就是是我们的数据!在使用TypeScript时我们编写的是将类型作为输入并输出其他类型的程序。

要想掌握TypeScript,我们需要对其不同类型的数据和数据结构有深入的了解。在接下来的几章中,我们将了解它们,并了解它们与我们在值的编程层面使用的概念之间的关系以及它们之间的区别。让我们开始吧!

五种类型

TypeScript为我们提供了5种主要类型:

  • primitive: 原始类型
  • literal: 字面量类型
  • data structure: 数据结构类型
  • union: 联合类型,也称并集
  • intersection: 交叉类型,也称交集

Primitive—原始类型

原始类型是我们最熟悉的类型。我们一直使用它们来注释日常TypeScript代码中的变量和函数。以下是基本类型列表:

type Primitives =
  | number
  | string
  | boolean
  | symbol
  | bigint
  | undefined
  | null;

除了objectfunction之外,其它所有JavaScript的值都属于原始类型。一些原始类型包含无数的子类型,比如numberstring 。但其中两个只有一个子类型:nullundefined 。这种特性也引出了我们的第二种类型:Literal—字面量类型

Literal—字面量类型

字面类型是“精确”类型,包含一个可能的值。

type Literals =
  | 20
  | "Hello"
  | true
  | 10000n
  /* | ... */;

类型为20的变量只能分配给值20,类型为“Hello”的变量只能指定给值“Hello”,等等。

const twenty: 20 = 20; // ✅ works!
const hundred: 20 = 100;
//          ^ ❌ `100` isn't assignable to the type `20`.

值和类型属于两个不同的世界——它们分别存在,不能在一个表达式中混合使用。一个显著的区别是:type five = 2 + 3 是❌的,而 const five = 2 + 3是✅的。

当将字面量类型放在联合类型中,用以描述包含有限组可能值的类型,如红绿灯类型TrafficLight=”green” | “red” | “yellow”, 字面量类型尤其有用。

Data Structure—数据结构类型

在TypeScript类型世界中,有四个内置的数据结构供我们使用:objectsrecordstuplesarrays

type DataStructures =
  | { key1: boolean; key2: number } // objects
  | { [key: string]: number } // records
  | [boolean, number] // tuples
  | number[]; // arrays
  • objects类型描述具有有限键集的对象,这些键包含可能不同类型的值。
  • recordsobjects相似,只是它们描述的对象具有未知数量的键,并且records中的所有值共享相同的类型。例如,在{[key:string]:number}中,所有值都是数字。
  • tuples描述具有固定长度的数组。每个索引可以有不同的类型。
  • arrays描述长度未知的数组。与records一样,所有值共享相同的类型。

Union—联合类型 && Intersection—交叉类型

到目前为止,我们所看到的一切似乎都与我们在值层面上习惯的概念有些相似,但联合和交叉是不同的。它们确实是特定于类型级别的,建立一个关于它们如何工作的良好思想模型是至关重要的,尽管有点挑战性。

以下是它们的最简单使用:

type Union = X | Y;

type Intersection = X & Y;

可以将

  • 联合类型X | Y读取为“X类型或Y类型”即并集
  • 交叉类型X & Y读为“X和Y类型的同时值”即交集

我们倾向于将|&视为运算符,但事实上,它们也是数据结构。

创建并集X | Y不会像运算符那样将X和Y转换为新的不透明类型。相反,可以理解为X | Y会将X和Y放在一个盒子中,以便我们稍后从中提取它们。在接下来的章节中,将看到我们甚至可以循环遍历联合类型中的每个类型。考虑到这一点,|看起来更像是一种向某种“联合”数据结构添加类型的方法。但到底是什么?

好吧,我们可以说联合类型相当于JavaScript中的集合的类型级别。为了更好地理解并集和交集类型,我需要介绍一个对TypeScript至关重要的概念:所有类型都是集合

所有类型都是集合

在TypeScript世界中有一个有趣的特性是:一个值可以属于多个类型。例如:

const a: number = 2 // 2可以分配给number
const b: 2 = 2 // 2可以分配给字面量类型2
const c: 1|2|3 = 3 // 2可以分配给联合类型1|2|3

该特性称为子类型。这意味着类型可以包含在其他类型中,或者换句话说,类型可以是其他类型的子集

这意味着不仅仅是联合类型,TypeScript世界中所有类型都是集合!类型可以包含其他类型相互重叠互斥

包含—可分配性

字面量类型“Hi”和字面量类型“Hello”都包含在string类型中,因为它们都是string大家族中的一部分:

<Type>Level TypeScript-2.类型只是数据

我们说“Hi”“Hello”string子类型/子集,而string是它们的超类型/超集。这意味着您可以将“Hi”“Hello”类型的变量分配给string类型的变量,反之则不行:

let hi: "Hi" = "Hi";
let hello: "Hello" = "Hello";

let greeting: string;

greeting = hi; // ✅ type-checks!
greeting = hello; // ✅ type-checks!

hello = greeting; // ❌ doesn't type-check!

我们还可以说“Hi”“Hello”可分配string

可分配性的概念在TypeScript中无处不在。大多数类型错误都会告诉您某些类型不能分配给其他类型。当您开始将类型视为值的集合时,可赋值性变得更加直观—— “A可赋值给B”仅仅意味着“集合B包括集合A中的所有值” ,或者**“A是B的子集”**。

互斥

比如stringnumber是互斥的:它们不重叠,因为没有值可以同时属于两个集合。

<Type>Level TypeScript-2.类型只是数据

这就是为什么不能将string类型的变量分配给number类型的变量,反之亦然:

let greeting: string = "Hello";
let age: number = greeting; // ❌ doesn't type-check.

重叠

两种类型有时会部分重叠。在这种情况下,它们既不互斥,也不具有子类型关系:

<Type>Level TypeScript-2.类型只是数据

在处理联合类型时经常发生重叠, 比如 "green" | "orange"和 "orange" | "red" 就有重叠部分:

<Type>Level TypeScript-2.类型只是数据

让我们为这两种联合类型命名:

type CanCross = "green" | "orange";
type ShouldStop = "orange" | "red";
❓ 现在,我们可以将`CanCross`类型的变量分配给`ShouldStop`类型的变量吗?
let canCross = "orange" as CanCross; // ✅
let shouldStop = "orange" as ShouldStop; // ✅
canCross = shouldStop;
//       ❌ ~~~~~~~~~ type 'red' isn't assignable to the type `green` | 'orange'
shouldStop = canCross;
//         ❌ ~~~~~~~ type 'green' isn't assignable to the type `orange` | 'red'

答案是:不可以。因为即使CanCrossShouldStop都包含“orange”,它们也不能互相分配,因为它们并不完全重叠。这可能有点违反我们的第一直觉,但是请记住,TypeScript不知道也不关心变量包含什么值。它只关系它的类型!

联合类型 | — 并集

如果你了解一点集合论,你就会知道两个集合的并集是包含这两个集合,所以A|B是包含所有A类型值和所有B类型值的类型:

<Type>Level TypeScript-2.类型只是数据

我们可以将任意两个集合连接在一起,包括其他联合类型!例如,我们可以将前面示例中的CanCrossShouldStop连接到TrafficLight联合类型中:

<Type>Level TypeScript-2.类型只是数据

// this is equivalent to "green" | "orange" | "red"
type TrafficLight = CanCross | ShouldStop;

let canCross: CanCross = "green";
let shouldStop: ShouldStop = "red";

let trafficLight: TrafficLight;
trafficLight = shouldStop; // ✅
trafficLight = canCross; // ✅

TrafficLightCanCrossShouldStop超集。注意,TrafficLight中只有一次出现“orange”。这是因为集合不能包含重复项,所以联合类型也不能。

联合类型有助于创建嵌套类型的层次结构。由于我们总是可以在联合中放置两种类型,因此我们可以创建任意数量的子类型级别:

<Type>Level TypeScript-2.类型只是数据

❓ 此时,您可能会想,如果所有类型都可以属于其他类型,那么这种嵌套类型的层次结构何时停止?有没有一种类型是最终的boss,并且包含了宇宙中所有其他可能的类型?

是的,的确有一种这样的类型,它就是unknown

unknow — 嵌套层次结构的顶点/所有类型的超集

unknown包含您将在TypeScript中使用的每种类型。

<Type>Level TypeScript-2.类型只是数据

你可以将任何类型的值分配给 unknown:

let something: unknown;

something = "Hello";            // ✅
something = 2;                  // ✅
something = { name: "Alice" };  // ✅
something = () => "?";          // ✅

这很好,但这也意味着你不能对unknown的变量做很多事情,因为TypeScript对它包含的值一无所知!

let something: unknown;

something = "Hello";
something.toUpperCase();
//       ^ ❌ Property 'toUpperCase' does not exist
//            on type 'unknown'.

任何类型Aunknown的并集总是unknown。这是有意义的,因为根据定义,A已经包含在未知中:

A | unknown = unknown

<Type>Level TypeScript-2.类型只是数据

但是交集呢?

任何类型Aunknown的交集为类型A

A & unknown = A

这是因为集合A与集合B相交意味着提取既属于A也属于B的部分!因为任何类型A都在unknown内部,所以A&unknown结果是A

<Type>Level TypeScript-2.类型只是数据

交叉类型& — 交集

交集与并集正好相反:A&B是同时属于A和B的所有值的类型:

<Type>Level TypeScript-2.类型只是数据

交集对于object类型特别方便,因为两个对象AB的交集是一组具有所有A所有属性和B所有属性的对象

<Type>Level TypeScript-2.类型只是数据

这就是为什么我们有时使用交集&object类型合并在一起:

type WithName = { name: string };
type WithAge = { age: number };

function someFunction(input: WithName & WithAge) {
  // `input` is both a `WithName` and a `WithAge`!
  input.name; // ✅ property `name` has type `string`
  input.age; // ✅ property `age` has type `number`
}

❓ 但如果我们尝试将两种完全不重叠的类型相交,会发生什么?例如,stringnumber相交意味着什么?

<Type>Level TypeScript-2.类型只是数据

看起来string & number可能会给我们带来某种类型的错误,但实际上,这对类型检查器来说是有意义的!不重叠的两个类型相交的结果是空集。不包含任何内容的集合。

在TypeScript中,空集被称为never

never — 空集/集合层次结构的最底层/所有类型的子集

never不包含任何值,因此我们可以使用它来表示在运行时不应该存在的值。例如,总是抛出异常的函数将返回never类型的值:

function panic(): never {
  throw new Error("🙀");
}

const oops: never = panic(); // ✅永远不会执行到

这是因为使用oops的代码永远不会执行到!

上面的代码很好理解,但感觉never好像没啥用啊!

不好意思!在编写类型级代码时,我们无时无刻不在使用nevernever本质上是空联合类型。空类型对于类型级逻辑非常有用。我们将使用它从对象类型中移除键,从联合中过滤出项,表示不可能的情况等。

never的另一个有趣的特性是它是所有其他类型的子类型——它位于集合层次结构的最底层。这意味着您可以将类型为never的值分配给任何其他类型:

const username: string = panic(); // ✅ TypeScript is ok with this!
const age: number = panic(); // ✅ And with this.
const theUniverse: unknown = panic(); // ✅ Actually, this will always work.

如果将never放在具有现有联合类型的联合中,它将保持不变:

type U = "Hi" | "Hello" | never;
// is equivalent to:
type U = "Hi" | "Hello";

<Type>Level TypeScript-2.类型只是数据

这就像将一个空集合与另一个集合合并。任何类型Anever的并集都等于A

A | never = A

然而,如果你将a型与never相交,结果都是never

A & never = never

挑战

/**
 * Type the `move` function so that the `direction`
 * parameter can only be assigned to "backward" or "forward".
 */
namespace move {
  
  function move(direction: TODO) {
    // some imaginary code that makes the thing move!
  }

  // ✅
  move("backward")

  // ✅
  move("forward")

  // @ts-expect-error: ❌ not supported
  move("left")

  // @ts-expect-error: ❌ not supported
  move("right")
}
/**
 * `pickOne` takes 2 arguments of potentially different
 * types and return either one or the other at random.
 * Make  generic!
 */
namespace pickOne {

  function pickOne(a: TODO, b: TODO): TODO {
    return Math.random() > 0.5 ? a : b;
  }

  const res1 = pickOne(true, false);
  type test1 = Expect<Equal<typeof res1, boolean>>;

  const res2 = pickOne(1, 2);
  type test2 = Expect<Equal<typeof res2, 1 | 2>>;

  const res3 = pickOne(2, "some string");
  type test3 = Expect<Equal<typeof res3, 2 | "some string">>;

  const res4 = pickOne(true, 7);
  type test4 = Expect<Equal<typeof res4, true | 7>>;
}
/**
 * The `merge` function accepts an object of type `A`
 * and an object of type `B`, and return an object
 * with all properties of `A` and `B`.
 * Make it generic!
 */
namespace merge {
  function merge(a: TODO, b: TODO): TODO {
    return { ...a, ...b };
  }

  const res1 = merge({ name: "Bob" }, { age: 42 });
  type test1 = Expect<Equal<typeof res1, { name: string } & { age: number }>>;

  const res2 = merge({ greeting: "Hello" }, {});
  type test2 = Expect<Equal<typeof res2, { greeting: string }>>;

  const res3 = merge({}, { greeting: "Hello" });
  type test3 = Expect<Equal<typeof res3, { greeting: string }>>;

  const res4 = merge({ a: 1, b: 2 }, { c: 3, d: 4 });
  type test4 = Expect<
    Equal<typeof res4, { a: number; b: number } & { c: number; d: number }>
  >;
}
/**
 * Type `debounceFn` as a function with a `cancel` method on it.
 *
 * Hint: To tell TS a variable is a function, you can either
 * use the type `Function` or `(() => void)`.
 */
namespace debouncedFn {
  
  let debouncedFn: TODO

  debouncedFn = Object.assign(() => {}, { cancel: () => {} });

  // ✅
  debouncedFn();

  // ✅
  debouncedFn.cancel();

  // ❌ `unknownMethod` does not exist on `debouncedFn`.
  // @ts-expect-error
  debouncedFn.unknownMethod();

  // ❌ can't assign a string to `debouncedFn`.
  // @ts-expect-error: ❌
  debouncedFn = "Hello";
}
/**
 * Type the `stringify` function to take any kind of input.
 *
 * Don't use `any`!
 */
namespace stringify {
  
  function stringify(input: TODO) {
    return input instanceof Symbol ? input.toString() : `${input}`;
  }

  stringify("a string");    // ✅
  stringify(12);            // ✅
  stringify(true);          // ✅
  stringify(Symbol("cat")); // ✅
  stringify(20000n);        // ✅
}
/**
 * Type the `exhaustive` function so that it cannot be 
 * called except in unreachable code branches.
 */
namespace exhaustive {
  
  function exhaustive(...args: TODO) {}

  const HOURS_PER_DAY = 24
  // Since `HOURS_PER_DAY` is a `const`, the next
  // condition can never happen
  // ✅
  if (HOURS_PER_DAY !== 24) exhaustive(HOURS_PER_DAY);

  // Outside of the condition, this should
  // return a type error.
  // @ts-expect-error ❌
  exhaustive(HOURS_PER_DAY);

  const exhautiveCheck = (input: 1 | 2) => {
    switch (input) {
      case 1: return "!";
      case 2: return "!!";
      // Since all cases are handled, the default
      // branch is unreachable.
      // ✅
      default: exhaustive(input);
    }
  }

  const nonExhautiveCheck = (input: 1 | 2) => {
    switch (input) {
      case 1: return "!";
      // the case where input === 2 isn't handled,
      // so `exhaustive` shouldn't be called.
      // @ts-expect-error ❌
      default: exhaustive(input);
    }
  }
}
转载自:https://juejin.cn/post/7205016004924014629
评论
请登录