TypeScript与JavaScript不同之处系列(八) ===> 高级类型

版权声明:原创文章,如想转载,请注明原文网址,注明出处;否则,禁止转载;谢谢配合! https://blog.csdn.net/c_kite/article/details/85457753

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

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

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性. 例如, 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();

联合类型(Union Types)

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

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

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

类型保护

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

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();
}

用户自定义的类型保护

上面的例子中我们不仅在第一个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类型.

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}'.`);
}

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'
}

判断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";
}

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区别对待

可选参数和可选属性

使用了 --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'

类型别名

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

接口 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其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名.

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


字符串字面量类型

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

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 ...
}

数字字面量类型

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

function rollDie(): 1 | 2 | 3 | 4 | 5 | 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;
    }
}

完整性检查

此节目的: 当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 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
    }
}

多态的 this类型

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

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

由于这个类使用了 this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

索引类型

此节目的: 如何取动态类型

索引类型查询操作符

索引类型查询使用关键字keyof, 见下面例子

interface Person {
    name: string;
    age: number;
}

let personProps: keyof Person; // 'name' | 'age'

索引访问操作符

索引访问操作符使用对象[索引]的形式, 见下面的例子

interface Person {
  name: string;
  age: number;
}

let a: Person['name'] = '123';
let b: Person['name'] = 123; // error

总结:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
// 注意此处的 T[K] 是类型(像int或者long的一种类型表达形式, 因此如果返回的数组, 还可以这么表示 T[k][])
    return o[name]; // o[name] 是类型 T[K]
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

映射类型

此节解决的问题是, 从旧类型中创建新类型:

interface Person {
    name: string;
    age: number;
}

例如, 想把上面的类型变成全部只读或者全部可选, 就方法是重新定义

// 旧方法
interface PersonPartial {
    name?: string;
    age?: number;
} // 可选

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
} // 只读

TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性, 例子如下

// (keyof见上一小节, 索引类型里有谈)
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

type Nullable<T> = {
	[P in keyof T]: T[P] | null
}


type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;


-----------------简单一点的例子-----------------

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了 for … in。 具有三个部分:

  1. 类型变量 K,它会依次绑定到每个属性。
  2. 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

上面的简单例子转换完为

type Flags = {
    option1: boolean;
    option2: boolean;
}

由于关键字in内部使用了for…in, 因此有很多用用法, 见下面的例子

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> – 从T中剔除可以赋值给U的类型。
  • Extract<T, U>– 提取T中可以赋值给U的类型。
  • NonNullable<T> – 从T中剔除null和undefined。
  • ReturnType<T> – 获取函数返回值类型。
  • InstanceType<T> – 获取构造函数类型的实例类型。
// 例
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

猜你喜欢

转载自blog.csdn.net/c_kite/article/details/85457753