hardfist/stackoverflow

typescript的fresh literal type的坑

Opened this issue · 0 comments

今天公司小伙伴分享Typescript实践时,提到的一个小坑就是对象字面量的赋值问题。本文结合规范谈谈关于fresh object literal
type的小坑。

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: 'sss',
    area: 10,
  };
}
const obj: SquareConfig = { colour: 'red', width: 100 }; // 1.报错
let mySquare = createSquare({ colour: 'red', width: 100 }); // 2.报错
const obj2 =  { colour: 'red', width: 100 }
createSquare(obj2); // 3.不报错
createSquare({ colour: 'red', width: 100 } as SquareConfig) // 4.不报错

上面这个case就是Ts中使用对象字面量常常碰到的一个坑。同一个对象为什么通过第一种和第二种的方式调用会报错,通过第三种和第四种的方式调用却不会报错呢。

其实上面的报错是Typescript进行assignment compatibility检测出来的。

Typescript实际存在着两种兼容性,子类型兼容性(subtype compatibility)和赋值兼容性(assignment
compatibility)。上例子中的兼容性正对应了赋值兼容性。

对于赋值语句和参数调用时都会触发赋值兼容性检测。

考虑赋值操作 T = S, S可以赋值给T的一个条件是 S相对于T并没有excess properties。

这里引入了excess properties的概念,Ts引入excess
properties的目的就在于对于对象字面量的赋值,比相对于普通的右值进行更加严格的检查,以防止用户的拼写或者添加了多余属性错误。

excess properties概念定义如下:

S相对于T存在 excess properties,当且仅当:

  1. S是一个fresh object literal type,
  2. S 有一个或多个T中不expect的属性

规范毕竟是规范,概念层出不穷,这里又引入了两个额外的概念

fresh object literal type 和expect

首先解释下expect,我们说一个属性P被类型T expect 当且仅当满足如下之一条件:

  1. T 不是对象(object),联合(union),或者交叉(Intersection)类型
  2. T 是对象类型且
    1. T 存在和P同名的属性
    2. T 存在string或者index signature 如 { [key:string]:string}
    3. T 不存在属性
    4. T 是全局的 Object
  3. T是一个 union或者 intersection 类型且 P 是T组成type的expect 属性 (这里已经递归了,expect的判定是个递归算法)

对象字面量如 { colour: 'red', width: 100 } 的类型为 fresh object literal type
,我们可以通过widen或者assertion的方式将 fresh object literal type
的freshness擦除。看看这里又引入了widen和assertion的概念

widen的概念是Ts为了帮助用户做自动类型推断而引入的。

  var name = "Steve"; // 自动推断name 为string

实际上Ts的类型推倒稍显复杂,你能猜到下面的类型推倒结果吗?

var name1 = 'steve'; // string
const name2 = 'hello'; // 'hello'
const a = null; // any
var b = null; // any
const c = undefined; // any
var d = undefined; // any
const obj = { a: 'hello', b: 2} // { a: string, b: number, c: any}

widen type的一条规则是所有undefined和null的地方都被推倒为any。具体的全部规则我也不知道啊。

讲到这里我们终于可以解决开始的题目了。

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: 'sss',
    area: 10,
  };
}
const obj: SquareConfig = { colour: 'red', width: 100 }; // 1.fresh object type
let mySquare = createSquare({ colour: 'red', width: 100 }); // 2. fresh object type
const obj2 = { colour: 'red', width: 100 }; //3. widen form :{ colour: string, width: number}
const obj4 = 'string';
createSquare(obj2);
createSquare({ colour: 'red', width: 100 } as SquareConfig); // 4. assertion

上例中1和2都是属于S属于fresh object type且含有excess property即 colour
,3属于通过widen消除freshness,4属于通过assertion消除freshness 。

那么我们还有其他方法来解决1和2中的报错吗?

当然可以只要破坏 S相对于T存在excess property的两个必要条件之一即可

  1. S是一个fresh object literal type,
  2. S 有一个或多个T中不expect的属性

方法3和方法4都是破坏了条件1,我们也可以通过破坏条件2解决报错问题。即S中的属性都被T expect即可,回顾属性P被类型T
expect条件。我们任意满足四个条件之一即可。

const obj3: { [key: string]: any; color?: string; width?: number } = {
  colour: 'red',
  width: 100,
}; // index signature

const obj4: {} =  {
  colour: 'red',
  width: 100,
}; // has no properties

const obj5: Object = {
  colour: 'red',
  width: 100,
}; // global  Object