首页 > 解决方案 > Typescript 是否允许破坏类型安全的突变?

问题描述

在下面的代码片段中,是否证明 Typescript 不能以违反其类型约束的方式警告严格类型对象的突变?

type Animal = {
  name: 'cat'
  noise: 'meow'
} | {
  name: 'dog'
  noise: 'woof'
};


const f = (animal: Animal) => {
  // Why is this allowed? Typescript still considers this to be of type 'Animal'
  // after this mutation, even though its value no longer assignable to any variant
  // of Animal.
  animal.noise = 'woof';

  // ❌ - Typescript failed to update animal's type when we mutated it, so now
  // it mistakenly continues to believe it is assignable to Animal everywhere!
  // Now its incorrectly typed data can spread throughout the application,
  // breaking type safety, potentially everywhere...
  const anotherAnimal: Animal = animal;

  // ✅ - Typescript correctly prevents us from assigning this combination of fields
  // to type Animal. But this is exactly the value we mutated to in our first example,
  // so why were we allowed to do that when the outcome is the same, invalid result?
  const animalMatchingPostMutationValue: Animal = {
    name: 'cat',
    noise: 'woof'
  }
}

const animal: Animal = { 
  name: 'cat',
  noise: 'meow',
}

f(animal)

在此处查看操场链接。

我认为应该防止这种情况是错误的吗?如果没有,除了简单地避免突变(也许readonly用来强制执行这种做法)之外,还有其他方法吗?

编辑

重做示例以避免强制转换。

标签: typescripttype-safetyunion-typesmutability

解决方案


你不应该as Animal使用

const animal: Animal = { 
  name: 'cat',
  noise: 'meow',
} as Animal

它没有意义,并使您的代码unsafe

请参阅文档

类型断言是告诉编译器“相信我,我知道我在做什么”的一种方式。类型断言类似于其他语言中的类型转换,但它不执行数据的特殊检查或重组。它没有运行时影响,仅由编译器使用。TypeScript 假定您,程序员,已经执行了您需要的任何特殊检查。

一旦你编写as Animal了 ,TypeScript 假定 anyAnimal可以分配给适当的属性:

const animal: Animal = {
  name: 'cat',
  noise: 'meow',
} as Animal

animal.name // "cat" | "dog"
animal.noise // "meow" | "woof"

它不再能够区分它是否是联合的一部分。

type Name = Animal['name'] // "cat" | "dog"
type Noise = Animal['noise'] // "meow" | "woof"

这就是类型断言不安全的原因,只有在没有其他选择时才应考虑。

突变在打字稿中是有点不安全的操作,因为 TS 除了一种情况外不会跟踪它们。

您可以在我的文章中找到有关 typescript 突变的更多信息:

TL;博士

尽量避免打字稿中的突变

与打字稿中的突变相关的答案列表

如何在打字稿中选择性地从一个部分分配给另一个

为什么我可以按字符串索引以获取属性值但不能设置它?

TypeScript:为什么我不能分配类型为 { a: "a", b: "b" } 的对象的有效字段

为什么 Typescript 说这个变量是“在它自己的初始化程序中直接或间接引用的”?

https://github.com/microsoft/TypeScript/pull/30769

keyof 和 React Redux 操作

无法动态设置接口对象的属性值

将 Object.entries reduce 转换为泛型类型

总结 主要问题是对象可变属性是协变的。考虑这个从Titian-Cernicova-Dragomir 'stalk偷来的例子:

type Type = {
    name: string
}

type SubTypeA = Type & {
    salary: string
}

type SubTypeB = Type & {
    car: boolean
}

let employee: SubTypeA = {
    name: 'John Doe',
    salary: '1000$'
}

let human: Type = {
    name: 'Morgan Freeman'
}

let student: SubTypeB = {
    name: 'Will',
    car: true
}


// same direction
type Covariance<T> = {
    box: T
}

let employeeInBox: Covariance<SubTypeA> = {
    box: employee
}

let humanInBox: Covariance<Type> = {
    box: human
}

/**
 * MUTATION 
 */
let test: Covariance<Type> = employeeInBox

test.box = student // mutation of employeeInBox

const result_0 = employeeInBox.box.salary // while result_0 is undefined, it is infered a a string

console.log({ result_0 })

result_0undefined而 TS 认为它是string. 为了修复它,只需使用readonly标志作为COvariance类型:

type Covariance<T> = {
    readonly box: T
}

现在它失败了。

解决方法,没有readonly

type Cat = {
  name: 'cat'
  noise: 'meow'
}

type Dog = {
  name: 'dog'
  noise: 'woof'
};

type Animal<Name, Noise> = { name: Name, noise: Noise }

const f = <
  CatName extends Cat['name'],
  CatNoise extends Cat['noise'],
  DogName extends Dog['name'],
  DogNoise extends Dog['noise'],
  Pet extends Animal<CatName, CatNoise> | Animal<DogName, DogNoise>
>(animal: Pet) => {

  animal.noise = 'woof'; // error

  const anotherAnimal: Pet = animal;

  // error
  const animalMatchingPostMutationValue: Animal = {
    name: 'cat',
    noise: 'woof'
  }
}
const animal: Animal = {
  name: 'cat',
  noise: 'meow',
}

f(animal) // ok

Playground 你可以让它逆变。看起来不太好,但很有效。

您可以使用 eslint 插件来强制执行readonly标志


推荐阅读