约束即类型、TypeScript 编程内参(一)

avatar
@腾讯科技(深圳)有限公司

本文是《约束即类型、TypeScript 编程内参》系列第一篇:约束即类型,主要记述 TypeScript 的基本使用和语法。

PS: 本文语境下的「约束」指的是「类型对值的约束」

一、了解 TypeScript

TS 大家都听说或者使用过,是 Angular 的官方语言,提供了静态类型检查,是 JavaScript 这门语言的超集,也就是说:

TS = JS + 静态类型检查

TS 今年开始火了,越来越多的 js 项目开始用 ts 来实现,因此有了一句广为流传的名言(捏他)

任何用 js 写的项目终将用 ts 重构

那么,你了解 ts 吗?类型本质上是对变量的约束,理解类型,首先要理解的是变量的值,然后 ......

本文是本系列的第一篇约束即类型,面向的是「有一定 JS 开发经验的学习者」 ,推荐前端/node工程师学习,建议跟随本文的代码边写边看,包教不教会。

二、初始化 TypeScript 项目

通过以下方式初始化一个 ts 项目并编译运行:

$ npm i -g typescript   # 安装 ts
$ mkdir my-ts-learn     # 搭建 playground
$ cd my-ts-learn     # 进入目录
$ tsc --init            # 初始化一个 ts 工程
$ echo "console.log('hello, ts');" >> h.ts # 创建代码
$ tsc       # 编译
$ node h    # 运行

💡💡💡

关于 tsconfig.json 我们会在本系列的其他篇目介绍,敬请期待 对于初学者,暂时先使用默认的配置就好

三、any 类型

「any 即无约束」它代表着任意,如下所示:

let a: any = 123;

a = {};
a = () => {};
a = '不会报错';

电视里常说 AnyScrtipt 指的就是无理由的在 TS 里大量使用 any;越是使用 any,则 ts 越像 js,不建议这样做,这样会失去使用 ts 的意义。

💡💡💡

类型本身就是对程序的证明

四、基本类型及类型推断

const num: number = 123;
const str: string = 'eczn';

很多情况下,我们并不需要显式地指明类型是什么,ts 会帮我们自动地进行「类型推断」,比方说下面这样写的话,ts 会自动推断出 val 的类型是 string:

let val = '123';
val = 123; // 不行,会报错

💡💡💡

好的 ts 代码总是这样的:大部分变量的类型是 ts 自动推断出来的,而不是程序员到处给变量加类型(这样就成 java 了)

五、对象类型

一般情况下,我们可以利用 interfacetype 声明来创造对 JS 对象的「约束」。

interface Person1 {
    name: string;
}

type Person2 = {
    name: string;
}

const p1: Person1 = { name: '001 号居民' };
const p2: Person2 = { name: '002 号居民' };

但是有一点要清楚,TS 的类型学名上叫做 结构化类型 Structral Type,跟其他语言里的 具名类型 Naming Type 不太一样,譬如说:

interface Person3 = {
    name: string;
}

const p1: Person1 = { name: '001 号居民' };
const p3: Person3 = p1;
// 在 naming type 的语言中,这样会报错
// 但在 ts 这种 structral type 的语言下,这样不会报错

简而言之,structral type 进行类型检查的时候比较的是两类型的结构,如果一样说明类型一样;而 naming type 仅是比较类型的标识符是不是一样,不一样则认为类型不合。

所以我更倾向于认为,structral type 的作用,其实对值的一种约束。

💡💡💡

type 和 interface 两者在很多情况下是可以等价互相转换的,但实际上两者是有很大不同的,文章系列后文会描述

六、函数类型

函数的类型由这三者描述:i 入参ii 返回值iii 上下文

interface Person { name: string; };

// 需注意,这里的 this 经过 ts 编译后会消失
// 同学们可以自行编译体会个中奥义
function sayName(this: Person, suffix: string) {
    return this.name + ' ' + suffix;
}

const 无名先生 = { sayName };
const 阿J = { name: 'j', sayName };

// 报错,无名先生无名,不满足 this 上下文类型约束
无名先生.sayName('先生');

// 这里 阿 J 有 name 属性,满足 this 约束
阿J.sayName('先生');

// 注意,箭头函数在语言定义上是没有上下文的
// 因此下面这个会报错
const test = (this: any, x: string): string => x;

当然,在类里面声明的类型,其 this 已经默认为其本身了:

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    
    sayName(suffix: string) {
        // 这里 ts 能正确识别 this 指的是 Person 类
        // 不会报错
        return this.name + ' ' + suffix;
    }
}

💡💡💡

很多时候我们不必吧函数的上下文也定义出来,一般都是这样定义函数的:

const func = (a: X): Y = { /* function body */ }

七、字面量类型

对于值来说,字面量类型是比基本类型更窄的约束,比如:

function sayHello(suffix: '先生') {
    return 'hello,' + suffix;
}
sayHello('先生'); // 满足字面量类型,不会报错
sayHello('女士'); // 不满足,类型不合

我们熟知的 number 类型也有对应的字面量类型,读者可以自行写 demo 验证推敲。

八、利用 typeof 动态地推断

ts 拓展了 js 语法里面的 typeof,使其可以在 ts 进行类型声明的时候获取某个变量的类型:

let num = 123;
type MyNumber = typeof num;
// MyNumber 指的是就是 number 类型

ts 很多情况下是不用声明类型的,ts 会自动推断的,比如下面 UP_POSITION 的类型 ts 会自动推断为类型 string

let UP_POSITION = '向上';

但是下面这种情况呢?

const UP_POSITION_2 = '向上';

这种情况下我们得到的类型是一个字面量类型 '向上',如下所示

鼠标放在 Test 上可以看到精确定义

造成这种原因的结果是 const 声明的变量不会再变了,因此出来的是字面量类型,而 let 声明的类型是不确定的,因此用的是 string

那如果,我一定要在 let 的情况下推断某变量的类型为字面量呢?可以这样:

let UP_POSITION = '向上' as const;
type Test = typeof UP_POSITION;
// Test 是 '向上'

as const 会告诉 ts 请勿扩大范围。

💡💡💡

仔细想一想下面三行代码以及背后的运作原理 🤔

let a = 123;
let t = typeof a; // "number"
type T = typeof a; // number

九、泛型

泛型的意义在于,他声明了类型的上下文环境,使类型可以作为参数进行传递,比如我想实现一个数学上的常函数 x => x,ts 实现如下(需要用到泛型):

常函数 x => x

这里的 ts 声明描述了:

  1. 这里 T 单独尖括号标出的意思是告诉 ts,接下来的 T 是泛型
  2. 函数入参 x 和函数返回值的类型是 T

当具体 ts 去推断 val 的类型的时候,就可以发现 val 是 id('123') 这时候 T = '123' 因此 val 的类型就是 '123'。

泛型无处不在,它是类型的拓展,我们一般利用泛型去定义 可拓展的数据结构/接口/类型, 如 js 一些原生类里面就有泛型的影子:

// 求和 arr 并结果将其以 promise 的形式包裹返回
function sum(arr: Array<number>): Promise<number> {
    const sum = arr.reduce((a, b) => a + b);
    return Promise.resolve(sum);
}

sum([1, 2, 3]).then(console.log);
// => 6

💡💡💡

泛型里的泛是宽泛的泛,而不是范式的范。

最后一例:利用泛型实现链表:

// 链表, 这里声明了泛型 T
class CommomList<T> {
    value: T;
    // 这里的意思几乎等价于下面这种写法,用于声明可能不存在的字段:
    // next: CommomList<T> | undefined;
    next?: CommomList<T>;
    
    // 构造函数
    constructor(value: T, next?: CommomList<T>) {
        this.value = value;
        this.next = next;
    }

    // 设置 next
    setNext(next: CommomList<T>) { // next 是下一个节点
        this.next = next;
        return next;
    }

    // 链表生长
    grow(value: T) { // value 下一个节点的值
        const t = new CommomList(value);
        this.setNext(t);
        return t;
    }

    // 递归地打印自己
    toString(): string {
        return this.next ?
            JSON.stringify(this.value) + ' -> ' + this.next.toString() :
            JSON.stringify(this.value) + ' -> null';
    }
}

const _1 = new CommomList(1);

_1.grow(2).grow(3).grow(4);

// _1 是头
console.log(_1.toString());

本篇末

本文说明了 TS 的基本概念和使用方式,以下是总结 CheckList:

  1. 理解类型的内涵「类型是一种对于值的约束」
  2. 理解基本类型、函数类型、对象类型、字面量类型
  3. 体会到「类型本身就是对程序的证明」的思想
  4. 初步认识 any 和 typeof
  5. 初步理解了泛型的作用

本文的下一篇是「构造类型抽象、TypeScript 编程内参(二)」,敬请期待