我爱编程

TS文档的搬运工

2018-05-24  本文已影响651人  laiyituan

你会玩Javascript多级继承吗?
你能说明白js的作用域吗?
曾经有个面试题,用console.log写一个方法,要求输入1,输出的是0,结果

const console = Math
console.log(1) // 0

实际上,JavaScript里面充斥着大量的彩蛋,消耗大家非常多的时间。
JavaScript这门语言一直以来为什么会表现得这么诡异呢?一定要看阮老师的这篇文章Javascript诞生记
我们再看看TS的代码长什么样子
Validation.ts

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

import { StringValidator } from "./Validation";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

仔细看你会发现,TS跟我们现在用的es6很像,我们学习TS并没有什么学习成本。
而TS无论可读性、可维护性、上手的容易程度、写代码的速度,都非常明显地优于JS。


TS入门

基础类型

布尔值、数字、字符串、数组、元组枚举anyvoidNull 和 UndefinedNever类型断言

let isDone: boolean = false; // 布尔值

// 数字类型,支持十进制和十六进制,二进制和八进制字面量
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;

let sentence: string = `Hello, my name is ${ name }.`; // 字符串类型

// 数组类型 类型+[]
let list: number[] = [1, 2, 3]; // 可以在元素类型后面接上 [],表示由此类型元素组成的一个数组
let list: Array<number> = [1, 2, 3];// 使用数组泛型,Array<元素类型>
// (number | string)[]这是联合类型和数组的结合
// any[] 任意类型和数组的结合

// 元组类型 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

// 枚举类型
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

// 任意类型
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

// void 表示没有任何类型
let unusable: void = undefined;// 声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null

// null和undefined 类型 Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
// never 类型 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

类型断言
语法
<类型>值
值 as 类型(JSX)

let strLength: number = (<string>someValue).length; //(尖括号)
let strLength: number = (someValue as string).length; // as语法

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

// error
function getLength(something: string | number): number {
    return something.length;
}
// error
function getLength(something: string | number): number {
    if (something.length) {
        return something.length;
    } else {
        return something.toString().length;
    }
}
// 使用类型断言,将something断言成 string
function getLength(something: string | number): number {
    if ((<string>something).length) {
        return (<string>something).length;
    } else {
        return something.toString().length;
    }
}
// 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
// error
function toBoolean(something: string | number): boolean {
    return <boolean>something;
}

类型推论
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

// 类型推论
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;// error TS2322: Type 'number' is not assignable to type 'string'.
// 等价于
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

联合类型
联合类型使用 | 分隔每个类型

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
myFavoriteNumber = true; // Type 'boolean' is not assignable to type 'string | number'.

变量声明

let 变量声明
const 常量声明
ts跟es6一样推荐使用 let 和 const,因为他们都是块级作用域;不存在变量提升;
var 作为一个全局变量,易造成环境的变量污染;

”变量提升“,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

”暂时性死区“(temporal dead zone,简称 TDZ),即只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

结构赋值

// 数组结构赋值
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
// 展开
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
// 对象的结构赋值
let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;
// 属性重命名
let { a: newName1, b: newName2 } = o;

结构赋值同样适用于函数声明

type C = { a: string, b?: number }
function f({ a, b }: C): void {
    // ...
}

函数类型
函数声明

function sum(x: number, y: number): number {
    return x + y;
}
// 输入多余的(或者少于要求的)参数,是不被允许的
sum(1, 2, 3); // error
sum(1); // error

函数表达式

let mySum = function (x: number, y: number): number {
    return x + y;
};
// 等价于
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};
// 注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。
// 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
// 在 ES6 中,=> 叫做箭头函数

可选参数

function buildName(x: string, y?: string): string {
    if (y) {
        return `${x}${y}`;
    } else {
        return x;
    }
}

注意,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了
参数默认值

function buildName(x: string, y: string = 'Cat') {
    return `${x}${y}`;
}

剩余参数

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理;
比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。

// 利用联合类型,我们可以这么实现
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

这时,我们可以使用重载定义多个 reverse 的函数类型

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

接口Interfaces

对行为的抽象,接口一般首字母大写。有的编程语言中会建议接口的名称加上 I 前缀

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

let tom: Person = {
    name: 'Tom',
    age: 25
};
// 定义的变量比接口少了一些属性是不允许的
let tom: Person = {
    name: 'Tom'
};
// 多一些属性也是不允许的
let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

可见,赋值的时候,变量的形状必须和接口的形状保持一致。
注意,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以;
有时我们希望不要完全匹配一个形状,那么可以用可选属性:
可选属性

interface Person {
    name: string;
    age?: number;
}
// ok
let tom: Person = {
    name: 'Tom'
};
// ok
let tom: Person = {
    name: 'Tom',
    age: 25
};
// oh no 这时仍然不允许添加未定义的属性
let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};
interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}
// ok
let tom: Person = {
    name: 'Tom',
    gender: 'male'
};
// not ok 
let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};
// 任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。

只读属性

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}
let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};
tom.id = 9527; // Cannot assign to 'id' because it is a constant or a read-only property.

注意:只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};
// ok
tom.id = 89757;

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

readonly vs const
最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly。
函数类型

interface SearchFunc {
  (source: string, subString: string): boolean;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

可索引的类型
支持两种索引签名:字符串和数字。
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
// NumberArray 表示:只要 index 的类型是 number,那么值的类型必须是 number
interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

类类型
继承接口
一个接口可以继承多个接口,创建出多个接口的合成接口

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型
可以同时具有上面提到的多种类型。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

接口继承类
参考文档:TS官方文档

上一篇下一篇

猜你喜欢

热点阅读