TypeScript与JavaScript不同之处系列 ===; 高级类型

Posted 刘翾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TypeScript与JavaScript不同之处系列 ===; 高级类型相关的知识,希望对你有一定的参考价值。

本系列目的: 列出TypeScript与javascript的不同点, 缩小文档内容, 提高学习速度. 原文档地址: https://www.tslang.cn/index.html

全系列目录

这节内容比较多, 但也很实用

文章目录

1. 高级类型

1.1. 交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性. 例如, Person & Serializable & Loggable同时是 PersonSerializableLoggable. 就是说这个类型的对象同时拥有了这三种类型的成员

// 例
function extend<T, U>(first: T, second: U): T & U 
    let result = <T & U>;
    for (let id in first) 
        (<any>result)[id] = (<any>first)[id];
    
    for (let id in second) 
        if (!result.hasOwnProperty(id)) 
            (<any>result)[id] = (<any>second)[id];
        
    
    return result;


class Person 
    constructor(public name: string)  

interface Loggable 
    log(): void;

class ConsoleLogger implements Loggable 
    log() 
        // ...
    

var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

1.2. 联合类型(Union Types)

联合类型表示一个值可以是几种类型之一. 我们用竖线(|)分隔每个类型,例如: number | string | boolean表示一个值可以是 numberstring,或 boolean.

// 例
function padLeft(value: string, padding: string | number) 
    // ...


let indentedString = padLeft("Hello world", true); // error

1.3. 类型保护

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为某种类型时怎么办

interface Bird 
    fly();
    layEggs();


interface Fish 
    swim();
    layEggs();


function getSmallPet(): Fish | Bird 
    // ...
    return 
      fly: () => console.log(132),
      layEggs: () => console.log(456)
    


// 普通js可以采用这种方式判断, 不过在ts里会报错
let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) 
    pet.swim();

else if (pet.fly) 
    pet.fly();


----------------------------------

// ts里采用断言的方式来判断
let pet = getSmallPet();

if ((<Fish>pet).swim) 
    (<Fish>pet).swim();

else 
    (<Bird>pet).fly();

1.4. 用户自定义的类型保护

上面的例子中我们不仅在第一个if使用了断言而且在else中也使用了断言(如果不使用会报错, Property ‘fly’ does not exist on type ‘Bird | Fish’. Property ‘fly’ does not exist on type ‘Fish’.). 那么这个自定义类型保护所解决的问题就是: 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了.

function isFish(pet: Fish | Bird): pet is Fish 
    return (<Fish>pet).swim !== undefined;


// 在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

...


if (isFish(pet)) 
    pet.swim();

else 
    pet.fly();


注意TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型.

1.5. typeof类型保护

typeof类型保护适用于"number""string""boolean""symbol", 当然使用刚刚自定义的类型保护也是可以的, 见下面的两个例子.

function isNumber(x: any): x is number 
    return typeof x === "number";


function isString(x: any): x is string 
    return typeof x === "string";


function padLeft(value: string, padding: string | number) 
    if (isNumber(padding)) 
        return Array(padding + 1).join(" ") + value;
    
    if (isString(padding)) 
        return padding + value;
    
    throw new Error(`Expected string or number, got '$padding'.`);




----------------------------------------------


// typeof
function padLeft(value: string, padding: string | number) 
    if (typeof padding === "number") 
        return Array(padding + 1).join(" ") + value;
    
    if (typeof padding === "string") 
        return padding + value;
    
    throw new Error(`Expected string or number, got '$padding'.`);

1.6. instanceof类型保护

instanceof类型保护是通过构造函数来细化类型的一种方式

// 例子
interface Padder 
    getPaddingString(): string


class SpaceRepeatingPadder implements Padder 
    constructor(private numSpaces: number)  
    getPaddingString() 
        return Array(this.numSpaces + 1).join(" ");
    


class StringPadder implements Padder 
    constructor(private value: string)  
    getPaddingString() 
        return this.value;
    


function getRandomPadder() 
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");


// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) 
    padder; // 类型细化为'SpaceRepeatingPadder'

if (padder instanceof StringPadder) 
    padder; // 类型细化为'StringPadder'

1.7. 判断null

这与在JavaScript里写的代码一致:

function f(sn: string | null): string 
    if (sn == null) 
        return "default";
    
    else 
        return sn;
    


-------------或者-------------------


function f(sn: string | null): string 
    return sn || "default";


2. null类型

TypeScript具有两种特殊的类型, nullundefined. 默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型. 如果想取消默认行为可以在编译时在命令后面加一个--strictNullChecks, 例如: tsc 文件名 --strictNullChecks

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null', ts会把 null和 undefined区别对待

2.1. 可选参数和可选属性

使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

// 可选参数
function f(x: number, y?: number) 
    return x + (y || 0);

f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

------------------------------------------------

// 可选属性
class C 
    a: number;
    b?: number;

let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

3. 类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型. 起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型, 使用type关键字声明别名.

// 例1
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name 
    if (typeof n === 'string') 
        return n;
    
    else 
        return n();
    



// 例2 -- 泛型
type Container<T> =  value: T ;



// 例3 -- 引用自己
type Tree<T> = 
    value: T;
    left: Tree<T>;
    right: Tree<T>;



// 例4 -- 与交叉类型一起使用
type LinkedList<T> = T &  next: LinkedList<T> ;

interface Person 
    name: string;


let people: LinkedList<Person>;
let s = people.name;
let s = people.next.name;



// 例5 --- 注: 类型别名不能出现在声明右侧的任何地方
type Yikes = Array<Yikes>; // error

3.1. 接口 vs. 类型别名

注意上面提到的是, 接口和类型别名 有时 很像, 那么他们的区别是什么呢.

1,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在interfaced上,显示它返回的是Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

type Alias =  num: number 
interface Interface 
    num: number;

declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

2, 类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名.

总结: 如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名.


4. 字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串.

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement 
    animate(dx: number, dy: number, easing: Easing) 
        if (easing === "ease-in") 
            // ...
        
        else if (easing === "ease-out") 
        
        else if (easing === "ease-in-out") 
        
        else 
          // ...
        
    


let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy"不在type类型里面

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): htmlImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element 
    // ... code goes here ...

5. 数字字面量类型

TypeScript还具有数字字面量类型。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 
    // ...


6. 可辨识类型

它具有3个要素:

  1. 具有普通的单例类型属性— 可辨识的特征
  2. 一个类型别名包含了那些类型的联合— 联合
  3. 此属性上的类型保护
interface Square 
    kind: "square";
    size: number;

interface Rectangle 
    kind: "rectangle";
    width: number;
    height: number;

interface Circle 
    kind: "circle";
    radius: number;


// 首先我们声明了将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

type Shape = Square | Rectangle | Circle;


// 现在我们使用可辨识联合:
function area(s: Shape) 
    switch (s.kind) 
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    

6.1. 完整性检查

此节目的: 当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle到 Shape,我们同时还想编译器通知我更新 area:

第一种方法, 首先是启用 --strictNullChecks并且指定一个返回值类型(上面有提到过, 如果开启了这个参数, 就不能随便把null和undefined进行赋值)

function area(s: Shape): number  // error: returns number | undefined
    switch (s.kind) 
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    

第二种方法, 使用 never类型

// assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型
function assertNever(x: never): never 
    throw new Error("Unexpected object: " + x);

function area(s: Shape) 
    switch (s.kind) 
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    


7. 多态的 this类型

多态的 this类型表示的是某个包含类或接口的子类型

class以上是关于TypeScript与JavaScript不同之处系列 ===; 高级类型的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript与JavaScript不同之处系列 ===; 类

TypeScript与JavaScript不同之处系列 ===; 函数

TypeScript与JavaScript不同之处系列 ===; 泛型

TypeScript与JavaScript不同之处系列 ===;枚举

TypeScript与JavaScript不同之处系列 ===; 命名空间, 三斜线指令

javaScript学习笔记(与c++等不同之处)