专栏首页全栈修仙之路遇到这些 TS 问题你会头晕么?

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

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

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

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 枚举中的枚举成员也可以引用其他已定义的枚举成员,具体示例如下:

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 已有的枚举成员计算而得。

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 字面量类型联合后产生的类型:

let value3: 1 | 2 = 1;
let value4: 1 | 2 = 2;

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

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") 的调用语句会出现错误,其对应的错误信息如下:

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,具体如下:

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 的,所以我们顺便来分析一下那个示例:

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 对象的成员方法来生产鸡蛋或牛奶:

let animalToFood: ((_: Chicken) => Egg) & ((_: Cow) => Milk) = function(
  animal: any
) {
  return animal.produce();
};

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

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

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

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

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 从上下文推断类型,从而减轻了程序员显式指定看起来显而易见类型的需求。比如:

var name = "Semlinker";

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

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

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)[]

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

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 类型中不被期望的一个或多个属性” 这个条件来解决报错问题:

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技术栈最新文章。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 在 TS 中如何减少重复代码

    相信有些读者已经听说过 DRY 原则,DRY 的全称是 —— Don’t Repeat Yourself ,是指编程过程中不写重复代码,将能够公共的部分抽象出来...

    阿宝哥
  • TypeScript 交叉类型

    TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

    阿宝哥
  • 在 TS 中如何处理特殊值

    添加特殊值的一种方法是创建一个新类型,该类型是一些特殊值的基本类型的超集,这些特殊值称为哨兵。

    阿宝哥
  • Node.js 项目 TypeScript 改造指南(二)

    最近笔者把一个中等规模的 Koa2 项目迁移到 TypeScript,和大家分享一下 TypeScript 实践中的经验和技巧。

    WecTeam
  • TypeScript 疑难杂症

    作者:阿伟 - 身在高楼心在北大荒,我就这副死样~https://zhuanlan.zhihu.com/p/82459341

    ConardLi
  • TypeScript 强大的类型别名

    类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

    coder_koala
  • 在 TS 中如何减少重复代码

    相信有些读者已经听说过 DRY 原则,DRY 的全称是 —— Don’t Repeat Yourself ,是指编程过程中不写重复代码,将能够公共的部分抽象出来...

    阿宝哥
  • 索引类型、映射类型与条件类型_TypeScript笔记12

    P.S.注意,不同于typeof面向值,keyof是针对类型的,而不是值(因此keyof obj不合法)

    ayqy贾杰
  • JAVA实例化泛型

    DH镔
  • [PYTHON] 核心编程笔记之四

    身份: 每一个对象都有一个唯一的身份标识自己,任何对象的身份可以使用内建函数id()来得到,这个值可以认为是该对象的内存地址(只读)

    py3study

扫码关注云+社区

领取腾讯云代金券