[FE] TypeScript关于对象字面量的属性检查

1. 背景

1.1 子类型

在类型理论中,子类型关系通过S <: T来表示,它表示ST的一个子类型。
子类型关系具有以下两个性质,

(1)自反性

S <: S

(2)传递性

S <: U    U <: T
----------------
     S <: T

对于记录类型来说,
假设有类型S = { a:A, b:B, c:C, ...}T = { x:X, y:Y, z:Z, ... }
S <: T,当且仅当,S包含的字段数多于T

这看上去有些不可思议,包含字段更多的类型S,反而“更小”,
这种性质被称为,广度子类型化(width subtyping rule)。

1.2 和类型

和类型是一种带标签的联合类型(tagged union),
通过给各分量增加标签,将它们放在了一起。

类型A与类型B的和类型,通常记为A + B,例如,

Addr = PhysicalAddr + VirtualAddr

通常通过类型标记,来区分Addr的不同分量,
例如,如果pa是一个PhysicalAddr,则inl pa是一个Addr
其中,inlinr可以看做是一个函数

inl : PysicalAddr -> PhysicalAddr + VirtualAddr
inr : VirtualAddr -> PhysicalAddr + VirtualAddr

一般的,一个类型A + B的元素,
是由标记为inlA元素,和标记为inrB元素组成的。

和类型相关的类型推导规则如下,

    a: A
------------
inl a: A + B
    b: A
------------
inr b: A + B

2. TypeScript

TypeScript中的interface和union types,
分别对应于上文提到的记录类型,与和类型

(1)记录类型
根据记录类型的广度子类型化规则,以下子类型关系成立,

{a:number, b:number} <: {a:number}
{a:number, b:number} <: {b:number}

即,包含更多字段的{a:number, b:number},具有“更小”的类型。

(2)和类型
又根据,和类型的推导规则,

 x: {a:number, b:number} 
--------------------------
x: {a:number} | {b:number}

即,如果x是类型为{a:number, b:number}的值,则它也是类型为{a:number} | {b:number}的值。

3. 问题

但实际上,以上的写法是有问题的,

type A = { a: number };
type B = { b: number };
type AB = A | B;

// Type '{ a: number; b: number; }' is not assignable to type 'A'.
// Object literal may only specify known properties, and 'b' does not exist in type 'A'.
const x: A = { a: 1, b: 1 };

function f(p: A) { }

// Argument of type '{ a: number; b: number; }' is not assignable to parameter of type 'A'.
// Object literal may only specify known properties, and 'b' does not exist in type 'A'.
f({ a: 1, b: 1 });

TypeScript这样的做法,容易使人造成困扰

因为,这样看起来,{a: number, b: number}不是{a: number}的子类型,
也不是{b: number}的子类型。

然而,如果不是子类型的话,为什么{a: number, b: number}类型的值,
可以安全的赋值给{a: number} | {b: number}呢?

const y: AB = { a: 1, b: 1 };  // OK !

TypeScript会出现这样的问题,其实源于它除了进行类型检查之外,
针对对象字面量还多做了一些检查,官网记为,Excess Property Checks。

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.

注:

(1)不采用对象字面量(Object literals)的写法是完全没有问题的

type A = { a: number };
type B = { b: number };
type AB = A | B;

const t = { a: 1, b: 1 };
const x: A = t;  // OK !

function f(p: A) { }
f(t);  // OK !

(2)在对象字面量后面使用as指定类型,就不会报错了

type A = { a: number };
type B = { b: number };
type AB = A | B;

const x: A = { a: 1, b: 1 } as A;  // OK !

function f(p: A) { }
f({ a: 1, b: 1 } as A);  // OK !

(3)字面量属性检查,会检查是否传入了过多的属性字段,我们看3个字段的情形

type A = { a: number };
type B = { b: number };
type AB = A | B;

// Type '{ a: number; b: number; c: number; }' is not assignable to type 'AB'.
// Object literal may only specify known properties, and 'c' does not exist in type 'AB'.
const x: AB = { a: 1, b: 1, c: 1 };

function f(p: A) { }

// Argument of type '{ a: number; b: number; c: number; }' is not assignable to parameter of type 'A'.
// Object literal may only specify known properties, and 'b' does not exist in type 'A'.
f({ a: 1, b: 1, c: 1 });

参考

类型和程序设计语言
Excess Property Checks
Union Types
Interfaces

你可能感兴趣的:([FE] TypeScript关于对象字面量的属性检查)