Uso de tipos de unión discriminados en TypeScript

El dilema del tiempo de compilación frente al tipo de tiempo de ejecución

TypeScript es un lenguaje escrito que se compila en JavaScript, que en sí mismo no tiene tipos más allá de los primitivos. Entonces, cuando un desarrollador escribe TypeScript, se transpila a JavaScript, que luego se ejecuta en tiempo de ejecución, como un navegador o Nodejs en el lado del servidor. El proceso de transpilación aprovecha la información de tipo estático para detectar una clase completa de errores potenciales, pero el código de tiempo de ejecución resultante sigue siendo solo JavaScript sin tipo.

Esto tiene algunas implicaciones que pueden sorprender a los desarrolladores que se mudan a TypeScript desde un lenguaje tipificado en tiempo de ejecución. Por un lado, no puede verificar el tipo de nada más que primitivos en tiempo de ejecución. en la práctica:

// primitives work...
const myNumber = 1;
const myString = 'some string';

typeof myNumber; // -> "number"
typeof myString; // -> "string"

// ...but more sophisticated types don't
type MyType = {
  name: string;
  func: () => number;
}

const myValue: MyType = {
  name: 'some name for my type',
  func: () => 123
}

typeof myValue; // -> "object", nothing more specific than that
复制代码

pregunta

Así que supongamos que tenemos dos tipos, CatDog, y un tipo Animalque es la unión de los dos:

// all animals in our case share some base attributes
type BaseAnimal = {
  name: string;
  isFluffy: boolean;
}

// cats meow
type Cat = BaseAnimal & {
  meow: () => string;
}

// dogs bark
type Dog = BaseAnimal & {
  bark: () => string;
}

type Animal = Cat | Dog;

const whiskers: Cat = {
  name: 'Whiskers',
  isFluffy: true,
  meow: () => 'MEOWWW!' // Whiskers meows loudly
}

const pupper: Dog = {
  name: 'Pupper',
  isFluffy: false,
  bark: () => 'awooo' // Pupper has a gentle awoo instead of a loud bark
}

console.log(whiskers.meow()) // -> "MEOWWW!"
console.log(pupper.bark()) // -> "awooo"
复制代码

Ahora escribamos algo de lógica comercial para que an Animalemita un sonido, ya sea a o CatDog:

const makeNoise = (animal: Animal): string => {
  // ???
}
复制代码

Como muestra la captura de pantalla, TypeScript realmente no puede ayudarlo aquí.

TypeScript 无法处理 meow 和 bark 方法

Debido al comportamiento de los tipos en tiempo de compilación frente a tiempo de ejecución, no puede hacer cosas como esta:

const makeNoise = (animal: Animal): string => {
  // Doesn't work because the type doesn't exist at runtime,
  // typeof will just return 'object'
  if (typeof animal === 'Dog') {
    return animal.bark();
  }

  // ...
}
复制代码

En cambio, puede hacer esto, pero TypeScript no está contento con eso:

const makeNoise = (animal: Animal): string => {
  if (typeof animal.bark === 'function') {
    return animal.bark()
  }

  if (typeof animal.meow === 'function') {
    return animal.meow()
  }

  return 'not implemented!'
}

console.log(makeNoise(whiskers)) // -> "MEOWWW!"
console.log(makeNoise(pupper)) // -> "awooo"
复制代码

La desgracia de TypeScript proviene del hecho .meow()de que los errores se ven .bark()así :Animal

TypeScript 无法处理部分准确的信息

Incluso si adopta este enfoque, ¿qué sucede si aumenta la complejidad? Digamos que queremos agregar un Cat.purr()método y a los tipos y agregarlos también a la salida. El código se vuelve más difícil de usar de inmediato, las declaraciones son más complejas y hay más espacio para que se cuelen errores.Dog.growl()``makeNoise``if

solución

让运行时“知道”我们的编译时类型的解决方案非常简单。我们所需要的只是类型的标签,以便能够唯一地识别每个变体,并根据该信息进行区分。 实际上,这意味着添加一个具有文字值的属性,该属性可用于在编译时和运行时识别每个变体。

注意命名。 只要名称在变体之间保持一致,就可以将属性称为任何名称。这里我们使用_t. 其他常用名称是typetagkind,带或不带下划线。它也可能与正在建模的领域更相关,因此在我们的动物案例中,我们可以使用类似species.

// cats meow
type Cat = BaseAnimal & {
  _t: 'cat', // <- the discriminator for cat
  meow: () => string;
}

// dogs bark
type Dog = BaseAnimal & {
  _t: 'dog', // <- the discriminator for dog
  bark: () => string;
}

type Animal = Cat | Dog;
复制代码

请注意,TypeScript 的值_t必须是唯一的,才能唯一标识每个变体。现在业务逻辑变得更简单了:我们需要做的就是检查 的值_t,TypeScript 编译器知道根据它自动缩小类型。例如,and Animalwith_t === 'dog'可以从缩小AnimalDog

还要注意assertNever助手。这里它用于确保所有不同的变体都在switch语句的分支中处理。

const assertNever = (n: never): never => {
  throw new Error('Should never happen')
}

const makeNoise = (animal: Animal): string => {
  switch (animal._t) {
    case 'cat':
      return animal.meow();
    case 'dog':
      return animal.bark();
    default:
      return assertNever(animal);
  }
}
复制代码

以下屏幕截图说明了编译器如何能够根据我们所在的代码分支从通用Animal到特定缩小范围。Cat``Dog

编译器可以根据代码分支缩小类型

实际示例——异步 HTTP 状态

虽然前面的示例很有希望是说明性的,但我们很少编写有关猫和狗的代码。一个更有用和完整的示例可能是 SPA 应用程序中获取数据的熟悉情况:呈现不同的状态。

我们想象中的应用程序以干净状态开始,然后执行异步 HTTP API 调用以获取一些数据,然后根据请求发生的情况呈现结果成功或错误。这是一个最小的例子:

type Data = {
  items: string[];
};

type State =
  | { _t: "initial" }
  | { _t: "loading" }
  | { _t: "error"; err: Error }
  | { _t: "success"; data: Data };
复制代码

如您所见,我们可以使用可区分的联合来枚举不同的已知状态。这样,当我们编写渲染逻辑时,我们不需要做任何“if data then do something with data”类型的检查。相反,我们可以只检查 的值_t,编译器知道根据它来缩小它的范围。

再次注意,_t只要变体之间的名称一致,它也可以称为任何其他名称。在这种情况下,另一个合理的鉴别器名称示例可能是status.

该示例是在反应中,但也可以在任何其他渲染模式中完成。

const renderer = (state: State) => {
    switch (state._t) {
      case "initial":
        return <p>Click button to start</p>;
      case "loading":
        return <p>Loading...</p>;
      case "error":
        return (
          <div>
            <h3>Oops, error happened!</h3>
            <p>{state.err.message}</p>
          </div>
        );
      case "success":
        return (
          <div>
            <h3>Here's your data:</h3>
            <ol>
              {state.data.items.map((i) => (
                <li key={i}>{i}</li>
              ))}
            </ol>
          </div>
        );
    }
  }
复制代码

其他用例

我发现有区别的联合在操作结果需要在高层次上分类为成功或失败的情况下特别有用(提示:Either monad是一个非常有用的模式),以及成功和/的不同变体或分类更深的失败。所以想想这样的事情:

type Failure = LogicFailure | DatabaseFailure;

type Success = SuccessWithData | SuccessWithoutData;

type Result = Failure | Success;
复制代码

Supongo que te gusta

Origin juejin.im/post/7222288378460733498
Recomendado
Clasificación