为什么 TypeScript 不会抱怨这个?

huangapple go评论141阅读模式
英文:

Why Typescript doesn't complain about this?

问题

我很困惑,当我在useState hook中将超集类型分配给其子集类型时,为什么 TypeScript 不会抱怨类型不匹配?如果这是有意的,应该如何为useState hook 添加类型以使其正确报错?

interface Animal {
	name: string;
	food: string;
	legs: number;
}

interface Dog {
	name: string;
	food: string;
}

const animal: Animal = {
	name: 'animal',
	food: 'animal food',
	legs: 4,
};

function App() {
	const [data, setData] = useState<Dog>(animal);
    ...
}
英文:

I'm so confused, when I assign a superset type to their subset type in the useState hook, why would Typescript not complain about mismatched type? And if it's intended, how should I type the useState hook to make it complain properly?
`

interface Animal {
	name: string;
	food: string;
	legs: number;
}

interface Dog {
	name: string;
	food: string;
}

const animal: Animal = {
	name: &#39;animal&#39;,
	food: &#39;animal food&#39;,
	legs: 4,
};

function App() {
	const [data, setData] = useState&lt;Dog&gt;(animal);
    ...
}

答案1

得分: 3

静态类型检查器可以在比较类型时使用类型的名称(名义类型)或结构(结构类型),例如在检查一个类型是否是另一个类型的子类型时。

TypeScript 使用结构类型系统。

详见类型兼容性

TypeScript中的类型兼容性基于结构子类型。结构类型是一种仅基于它们的成员关系来关联类型的方式。这与名义类型形成对比。

TypeScript结构类型系统的基本规则是:如果y至少具有与x相同的成员,则xy兼容。

对比名义类型

名义类型意味着仅当它们的声明命名相同类型时,两个变量才是类型兼容的。例如,在C语言中,同一翻译单元中具有不同名称的两个结构类型永远不会被视为兼容,即使它们具有相同的字段声明。

像C++、Java和Swift等语言主要采用名义类型系统。

// 伪代码:名义系统
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // 错误!

像Go、TypeScript和Elm等语言主要采用结构类型系统。

// 伪代码:结构系统
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // 成功!

一种在TypeScript中声明名义类型的方式:

interface Animal {
    name: string;
    food: string;
    legs: number;
}

interface Dog {
    name: string;
    food: string;
}

type Brand<T, K> = T & { __brand: K }

type AnimalNominal = Brand<Animal, 'animal'>;
type DogNominal = Brand<Dog, 'dog'>;

const animal: AnimalNominal = {
    name: 'animal',
    food: 'animal food',
    legs: 4,
    __brand: 'animal'
};

const dog: DogNominal = {
    name: 'hot dog',
    food: 'dog food',
    __brand: 'dog'
}

const s1: AnimalNominal = animal // 可行
const s2: AnimalNominal = dog; // 错误

const s3: DogNominal = animal; // 错误
const s4: DogNominal = dog; // 可行

Playground链接

名义类型提案:https://github.com/Microsoft/Typescript/issues/202

utility-types包中的Brand<T, U>类型。

基于类型T定义U的名义类型

英文:

A static type checker can use either the name (nominal typing) or the structure (structural typing) of types when comparing them against other types (like when checking if one is a subtype of another).

TypeScript uses a structure typing system.

See Type Compatibility.

> Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing.

> The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x

V.S. Nominal typing

> Nominal typing means that two variables are type-compatible if and only if their declarations name the same type. For example, in C, two struct types with different names in the same translation unit are never considered compatible, even if they have identical field declarations.

Languages like C++, Java, and Swift have primarily nominal type systems.

// Pseudo code: nominal system
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // Error!

Languages like Go, TypeScript and Elm have primarily structural type systems

// Pseudo code: structural system
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let foo: Foo = new Bar(); // Works!

One way to declare nominal typing in TypeScript:

interface Animal {
    name: string;
    food: string;
    legs: number;
}

interface Dog {
    name: string;
    food: string;
}

type Brand&lt;T, K&gt; = T &amp; { __brand: K }

type AnimalNominal = Brand&lt;Animal, &#39;animal&#39;&gt;;
type DogNominal = Brand&lt;Dog, &#39;dog&#39;&gt;

const animal: AnimalNominal = {
    name: &#39;animal&#39;,
    food: &#39;animal food&#39;,
    legs: 4,
    __brand: &#39;animal&#39;
};

const dog: DogNominal = {
    name: &#39;hot dog&#39;,
    food: &#39;dog food&#39;,
    __brand: &#39;dog&#39;
}

const s1: AnimalNominal = animal // ok
const s2: AnimalNominal = dog; // error

const s3: DogNominal = animal; // error
const s4: DogNominal = dog; // ok

Playground Link

Nominal typing proposal: https://github.com/Microsoft/Typescript/issues/202

The Brand&lt;T, U&gt; type of utility-types package.

> Define nominal type of U based on type of T

答案2

得分: 0

要使赋值有效,必须符合类型兼容性规则。

TypeScript允许将超集类型分配给子集类型,因为超集类型包含子集类型所需的所有属性和值。

如果您希望TypeScript正确地抱怨不匹配的类型,您可以显式地为useState()钩子指定所需的类型。在这种情况下,您会希望将Animal指定为useState()钩子的类型,因为它与您正在初始化的animal对象匹配。

const [data, setData] = useState<Animal>(animal);

请记住,TypeScript类型检查在编译时静态执行。

英文:

the assignment to be valid due to type compatibility rules.

TypeScript allows assigning a superset type to a subset type because the superset type contains all the properties and values required by the subset type.

If you want TypeScript to properly complain about the mismatched type, you can explicitly type the useState() hook with the desired type. In this case, you would want to specify Animal as the type for the useState() hook, as it matches the animal object you are initializing it with.

   const [data, setData] = useState&lt;Animal&gt;(animal);

Remember that TypeScript type checks are performed statically at compile time,

huangapple
  • 本文由 发表于 2023年7月10日 17:48:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76652565.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定