前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >遇到这些 TS 问题你会头晕么?

遇到这些 TS 问题你会头晕么?

作者头像
阿宝哥
发布2020-03-25 17:23:08
5.2K0
发布2020-03-25 17:23:08
举报
文章被收录于专栏:全栈修仙之路全栈修仙之路

创建了一个“重学TypeScript”的微信群,想加群的小伙伴,加我微信 “semlinker”,备注重学TS。

一、可以为数字枚举分配越界值?

代码语言:javascript
复制
const enum Fonum {
    a = 1,
    b = 2
}

let value: Fonum = 12; // Ok
// Type '12' is not assignable to type '1 | 2'.
let value2: 1 | 2 = 12; // Error

相信很多读者看到 let value: Fonum = 12; 这一行,TS 编译器并未提示任何错误会感到惊讶。很明显数字 12 并不是 Fonum 枚举的成员。 为什么会这样呢?我们来看一下TypeScript issues 26362DanielRosenwasser 大佬的回答:

The behavior is motivated by bitwise operations. There are times when SomeFlag.Foo | SomeFlag.Bar is intended to produce another SomeFlag. Instead you end up with number, and you don’t want to have to cast back to SomeFlag. 该行为是由按位运算引起的。有时 SomeFlag.Foo | SomeFlag.Bar 用于生成另一个 SomeFlag。相反,你最终得到的是数字,并且你不想强制回退到 SomeFlag。

其实在 TypeScript 枚举中的枚举成员也可以引用其他已定义的枚举成员,具体示例如下:

代码语言:javascript
复制
enum Style {
  None = 0,
  Bold = 1,
  Italic = 2,
  Underline = 4,
  Emphasis = Bold | Italic,
  Hyperlink = Bold | Underline
}

在 Style 枚举类中,EmphasisHyperlink 的值是通过对已定义的枚举成员进行位或运算得出。为什么枚举会支持这种特性呢?这是因为枚举类型是 Number 类型的子类型,所以可以使用数值运算符来计算枚举的值。

了解完上述内容,我们再来看一下 let value: Fonum = 12; 这个语句,该语句 TS 编译器不会报错,是因为数值 12 是可以通过 Fonum 已有的枚举成员计算而得。

代码语言:javascript
复制
let value: Fonum = 
  Fonum.a << Fonum.b << Fonum.a |  Fonum.a << Fonum.b; // 12

let value2: 1 | 2 = 12; 这一行会提示 Type '12' is not assignable to type '1 | 2'. 的错误信息,这是因为 1 | 2 类型是是数字 1 字面量类型和数字 2 字面量类型联合后产生的类型:

代码语言:javascript
复制
let value3: 1 | 2 = 1;
let value4: 1 | 2 = 2;

二、函数类型也可以进行交叉运算?

代码语言:javascript
复制
type F1 = (a: string, b: string) => void;  
type F2 = (a: number, b: number) => void;

var f: F1 & F2 = (a: string | number, b: string | number) => { };  
f("hello", "world"); // Ok  
f(1, 2);             // Ok 
f(1, "test");  // Error

对于上述代码中的函数调用语句,只有 f(1, "test") 的调用语句会出现错误,其对应的错误信息如下:

代码语言:javascript
复制
No overload matches this call.
  Overload 1 of 2, '(a: string, b: string): void', gave the following error.
    Argument of type '1' is not assignable to parameter of type 'string'.
  Overload 2 of 2, '(a: number, b: number): void', gave the following error.
    Argument of type '"test"' is not assignable to parameter of type 'number'.

根据以上的错误信息,我们可以了解到 TypeScript 编译器会利用函数重载的特性来实现不同函数类型的交叉运算。在解决上述问题前,我们先来看一个维基百科上对交叉类型的描述:

Intersection types are useful for describing overloaded functions. For example, if number => number is the type of function taking a number as an argument and returning a number, and string => string is the type of function taking a string as an argument and returning a string, then the intersection of these two types can be used to describe (overloaded) functions that do one or the other, based on what type of input they are given. Contemporary programming languages, including Ceylon), Flow, Java), Scala), TypeScript, and Whiley) (see comparison of languages with intersection types), use intersection types to combine interface specifications and to express ad hoc polymorphism. Complementing parametric polymorphism, intersection types may be used to avoid class hierarchy pollution from cross-cutting concerns and reduce boilerplate code, as shown in the TypeScript example below.

交叉类型对于描述重载函数很有用。当代编程语言,包括 Ceylon,Flow,Java,Scala,TypeScript 和 Whiley,使用交叉类型来组合接口规范并描述特定多态。特定多态(ad hoc polymorphism)是程序设计语言的一种多态,多态函数有多个不同的实现,依赖于其实参而调用相应版本的函数。因此,特定多态仅支持有限数量的不同类型。函数重载乃至运算符重载也是特定多态的一种。

了解完交叉类型的相关知识,我们来着手解决上述问题,这里我们可以定义一个新的函数类型 F3,具体如下:

代码语言:javascript
复制
type F1 = (a: string, b: string) => void;
type F2 = (a: number, b: number) => void;
type F3 = (a: number, b: string) => void;

var f: F1 & F2 & F3 = (a: string | number, b: string | number) => { };  
f("hello", "world"); // Ok
f(1, 2);             // Ok 
f(1, "test");  // Ok

现在前面的问题已经解决了,刚好维基百科上提供交叉类型的示例是基于 TypeScript 的,所以我们顺便来分析一下那个示例:

代码语言:javascript
复制
class Egg { private kind: "Egg" }
class Milk { private kind: "Milk" }

//produces eggs
class Chicken { produce() { return new Egg(); } }

//produces milk
class Cow { produce() { return new Milk(); } }

//produces a random number
class RandomNumberGenerator { produce() { return Math.random(); } }

//requires an egg
function eatEgg(egg: Egg) {
  return "I ate an egg.";
}

//requires milk
function drinkMilk(milk: Milk) {
  return "I drank some milk.";
}

上面的程序代码中定义了 Chicken,Cow 和 RandomNumberGenerator 三个类,每个类都有一个方法来返回 Egg,Milk 或 number 类型的对象。此外,还定义了 eatEgg 和 drinkMilk 两个函数来实现吃鸡蛋和喝牛奶的功能。接下来我们来创建一个特定多态函数 animalToFood,该函数会调用给定 animal 对象的成员方法来生产鸡蛋或牛奶:

代码语言:javascript
复制
let animalToFood: ((_: Chicken) => Egg) & ((_: Cow) => Milk) = function(
  animal: any
) {
  return animal.produce();
};

需要注意的是,以上代码定义的 animalToFood 函数,其类型是 ((_: Chicken) => Egg) & ((_: Cow) => Milk) ,为了保证交叉运算后的类型兼容性,我们需要设置 animal 参数的类型为 any,否则会出现类型不兼容的问题。

下面我们来看一下 animalToFood 方法是如何保证类型安全的:

代码语言:javascript
复制
const chicken = new Chicken();
const cow = new Cow();
const randomNumberGenerator = new RandomNumberGenerator();

console.log(chicken.produce()); // Egg { }
console.log(cow.produce()); // Milk { }
console.log(randomNumberGenerator.produce()); // 0.2626353555444987

console.log(animalToFood(chicken)); // Egg { }
console.log(animalToFood(cow)); // Milk { }
// Argument of type 'RandomNumberGenerator' is not assignable 
// to parameter of type 'Chicken'.
// Argument of type 'RandomNumberGenerator' is not assignable 
// to parameter of type 'Cow'
console.log(animalToFood(randomNumberGenerator)); //Error

console.log(eatEgg(animalToFood(chicken))); //I ate an egg.
// Argument of type 'Milk' is not assignable to parameter of type 'Egg'.
console.log(eatEgg(animalToFood(cow))); //Error
console.log(drinkMilk(animalToFood(cow))); //I drank some milk.
// Argument of type 'Egg' is not assignable to parameter of type 'Milk'.
console.log(drinkMilk(animalToFood(chicken))); //Error

三、赋值兼容性和多余属性检查是啥?

代码语言:javascript
复制
interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: "blue",
    area: 666
  };
}

// Type '{ colour: string; width: number; }' is not assignable to type 'SquareConfig'.
// Object literal may only specify known properties, but 'colour' does not exist in 
// type 'SquareConfig'.Did you mean to write 'color' ?
const obj: SquareConfig = { colour: "red", width: 888 }; // Error

// Argument of type '{ colour: string; width: number; }' is not assignable to parameter
// of type 'SquareConfig'.
// Object literal may only specify known properties, but 'colour' does not exist in 
// type 'SquareConfig'.Did you mean to write 'color' ?
let mySquare = createSquare({ colour: "red", width: 888 }); // Error
const obj2 = { colour: "red", width: 888 };
createSquare(obj2); // Ok
createSquare({ colour: "red", width: 888 } as SquareConfig); // Ok

Typescript 实际存在着两种兼容性,子类型兼容性(subtype compatibility)和赋值兼容性(assignment compatibility)。子类型和赋值兼容性要求源类型相对于其目标类型没有多余的属性。此检查的目的是检测对象字面量中是否包含多余或拼写错误的属性。

如果满足以下条件,则认为源类型 S 相对于目标类型 T 含有多余的属性。

  • S 类型是一种 fresh 对象字面量类型(fresh object literal type)且;
  • S 类型包含 T 类型中不被期望(expected)的一个或多个属性。

如果满足以下条件之一,则可以认为属性 P 在类型 T 中被期望(expected):

  • T 不是 object,union 或 intersection 类型。
  • T 是 object 类型且
    • T 存在和 P 同名的属性
    • T 存在字符串或数字索引签名
    • T 没有属性
    • T 是全局的 Object 类型
  • T 是一个联合或交叉类型,并且 P 至少在 T 的一个组成类型中被期望。

为对象字面量推断的类型被认为是 fresh 对象字面量类型。当对象字面量类型被扩展或作为类型断言中的表达式类型时,freshness(新鲜度)将消失。 下面我们再来介绍一下扩展类型(Widened Types)在某些情况下,TypeScript 从上下文推断类型,从而减轻了程序员显式指定看起来显而易见类型的需求。比如:

代码语言:javascript
复制
var name = "Semlinker";

对于以上代码,TS 编译器会将变量 ‘name’ 的类型推断为字符串类型,因为该类型是用于初始化它的值的类型。从表达式推断变量,属性或函数结果的类型时,源类型的扩展形式用作目标的推断类型。

下面示例显示了由扩展类型生成的推断的变量类型:

代码语言:javascript
复制
var a = null;                 // var a: any  
var b = undefined;            // var b: any  
var c = { x: 0, y: null };    // var c: { x: number, y: null }  
var d = [ null, undefined ];  // var d: (null | undefined)[]

介绍完上述知识,我们来分析一下前面的问题:

代码语言:javascript
复制
interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: "blue",
    area: 666
  };
}

// (1) fresh object literal type
const obj: SquareConfig = { colour: "red", width: 888 }; // Error 

// (2) fresh object literal type
let mySquare = createSquare({ colour: "red", width: 888 }); // Error

// (3) Widened form:{ colour: string, width: number }
const obj2 = { colour: "red", width: 888 };
createSquare(obj2); // Ok

// (4) Type assertion
createSquare({ colour: "red", width: 888 } as SquareConfig); // Ok

在以上示例中 (1) 和 (2) 属于 fresh 对象字面量类型且含有多余的 colour 属性。(3) 是因为对象字面量类型被扩展(widened),而 (4) 是因为类型断言使得对象字面量的新鲜度消失,这就破坏了 “源类型 S 相对于目标类型 T 含有多余的属性” 中的第一个条件,即 S 类型是一种 fresh 对象字面量类型(fresh object literal type)。所以 TypeScript 编译器就不会提示错误。

除了破坏第一个条件之外,我们也可以通过破坏第二个条件,即 “S 类型包含 T 类型中不被期望的一个或多个属性” 这个条件来解决报错问题:

代码语言:javascript
复制
const obj3: { [key: string]: any; color?: string; width?: number } = {
  colour: 'blue',
  width: 666,
}; // string index signature

const obj4: {} =  {
  colour: 'blue',
  width: 666,
}; // has no properties

const obj5: Object = {
  colour: 'blue',
  width: 666,
}; // global Object

const obj6: { color?: string; colour?: string; width?: number;} = {
  colour: 'blue',
  width: 666,
}; // same property

以上的内容相对比较难理解,这里我们只要记住多余的属性检查的目的,是检测对象字面量中是否包含多余或拼写错误的属性。

四、参考资源


欢迎小伙伴们订阅前端全栈修仙之路,及时阅读 Angular、TypeScript、Node.js/Java和Spring技术栈最新文章。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020/03/25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、可以为数字枚举分配越界值?
  • 二、函数类型也可以进行交叉运算?
  • 三、赋值兼容性和多余属性检查是啥?
  • 四、参考资源
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档