前言
TS 是什么
- TypeScript 是 JavaScript 的一个超集,意味着js本身的语法在ts里面也能跑的通。ts一方面是对js加上了很多条条框框的限制,另一方面是拓展了js的一些能力,主要提供了「类型系统」和「对 ES6 的支持」,它由 Microsoft 开发,代码开源于 GitHub 上。
- 关于作者:C#的首席架构师以及Delphi和Turbo Pascal的创始人安德斯·海尔斯伯格参与了TypeScript的开发 ,以及其他创造和维护 TypeScript 的开发者们
如何起步
1.安装 TypeScript
$ npm install -g typescript
2.编译 TypeScript 文件
$ tsc helloworld.ts
# helloworld.ts => helloworld.js
对于刚入门 TypeScript 的小伙伴,也可以不用安装 typescript,直接使用线上的 TypeScript Playground 来学习新的语法或新特性。
基本类型
「boolean:」定义布尔值类型
「number:」 定义数值类型
「string:」定义字符串类型
「void:」空值
「null undefined :」是所有类型的子类型
「never:」代表永远不会发生的类型
「any:」 任意值
「array:」数组
「tuple:」元组
「enum:」枚举
「object:」对象
Never
never 类型是 TypeScript 中的底层类型, 是一个可靠的,代表永远不会发生的类型。
- 一个从来不会有返回值的函数(eg:如果函数内含有
while(true) {}); - 一个总是会抛出错误的函数(eg:
function foo() { throw new Error('Not Implemented') },foo的返回类型是never); - 可以利用类型收窄和 never 类型的特性来进行全面性检查 [后续会有同学做类型收窄 相关的分享]
type Foo = string | number; //使用 type 创建类型别名
function youHappyIsOk(foo: Foo) {
if(typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if(typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean;
然而他忘记同时修改 youHappyIsOk 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保youHappyIsOk 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:「使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。」
Any [任意值]
any 类型用来表示允许赋值为任意类型
因为普通类型,在赋值过程中改变类型是不被允许的,而any 类型,则允许被赋值为任意类型,同时「变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型」
Enum[枚举]
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
枚举使用 enum 关键字来定义:
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从1开始编号:
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; // 2
或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Blue; // 4
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 显示'Green'因为上面代码里它的值是2
Tuple[元组]
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string和number类型的元组。
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
字面量类型
- 「字符串字面量类型」: 你可以使用一个字符串字面量做为一个类型
let foo: 'Hello'; //在这里,我们创建了一个被称为 foo 变量,它仅接收一个字面量值为 Hello 的变量
foo = 'hi'; // Error: 'hi' 不能赋值给类型 'Hello'
它们本身并不是很实用,但是可以在一个联合类型中组合创建一个强大的(实用的)抽象:用来约束取值只能是某几个字符串中的一个。
type Happy = 'smile' | 'laugh' ;
function people(age: number, mood: Happy) {
// ...
}
people(1, 'smile'); // ok
people(1, 'sad'); // Error
上例中,我们使用 type 定了一个字符串字面量类型 Happy,它只能取三种字符串中的一种。
注意,「类型别名与字面量类型都是使用 type 进行定义。」
- 「其他字面量类型」: TypeScript 同样也提供
boolean和number的字面量类型:
type OneToFive = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;
类型推论
TypeScript 会在没有明确的指定类型的时候推测出一个类型,即类型推论。
「如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 「any」 类型而完全不被类型检查」
let lunckyNumber = 'seven';
lunckyNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
事实上,它等价于:
let lunckyNumber: string = 'seven';
lunckyNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
对比 any、never、void
- any是任意的值
- void是不能有任何值
- never永远不会有返回值
「egs:」
let isHappy: boolean = true;
let luckyNumber: number = 7;
let myName: string = 'hello world';
let sentence: string = `Hello, my name is ${myName}.`; // 模板字符串
let unusable: void = undefined; //void 类型的变量,只能赋值为 undefined、null
let u: undefined = undefined;
let n: null = null;
let num: number = undefined; //null undefined 是所有类型的子类型,即可以被赋值给任意类型的变量
let power: any; //power = '123'; power = 123;...
let foo: never; // 可用做类型注解,但never类型仅能被赋值给另外一个 never
类型注解
类型注解使用 :TypeAnnotation 语法。简单说就是对程序的函数、方法、子过程、以及变量等给出其类型,[基本类型] 里的例子其实也都包含了类型注解
eg:
const num: number = 123; //变量的类型注解
function identity(num: number): number { //函数参数及返回值的类型注解
return num;
}
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型
let luckyNumber: string | number;
luckyNumber = 'seven';
luckyNumber = 7;
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们「只能访问此联合类型的所有类型里共有的属性或方法」:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
length 不是 string 和 number 的共有属性,所以会报错
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型
对象的类型 — 接口 Interfaces
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。【接口一般首字母大写】 在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)【后续会有同学专门分享class】。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。
定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom'
};
// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
// Property 'age' is missing in type '{ name: string; }'.
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
所以,「赋值的时候,变量的形状必须和接口的形状保持一致」。
- 「可选属性」:有时我们希望不要完全匹配一个形状,那么可以用可选属性
可选属性的含义是该属性可以不存在,这时「仍然不允许添加未定义的属性」
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
- 「任意属性」
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
使用 [propName: string] 定义了任意属性取 string 类型的值
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
「一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集」
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
//
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
name: string;
age?: number;
[propName: string]: string | number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
- 「只读属性」
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};
tom.id = 9527;
// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
「只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候」
数组的类型
「类型 + 方括号」
let fibonacci: number[] = [1, 7, 3, 5, 7];
以上写法表示数组的项中「只允许」出现 number 类型
数组泛型
使用数组泛型(Array Generic) Array<elemType> 来表示数组:
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
扩展:泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性【后续会有同学做泛型的详细分享】
「用接口表示数组」-常用来表示「类数组」
类数组(Array-like Object)不是数组类型,比如 arguments:
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。
事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:
function sum() {
let args: IArguments = arguments;
}
其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
any 在数组中的应用
//用 any 表示数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
函数的类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)
// tips: 输入多余的(或者少于要求的)参数,是不被允许的
function sum(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 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用接口定义函数的形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
可选参数
前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用 ? 表示可选的参数:
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
需要注意的是,可选参数必须接在必需参数后面。换句话说,「可选参数后面不允许再出现必需参数了」
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,「TypeScript 会将添加了默认值的参数识别为可选参数」:
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
此时就不受「可选参数必须接在必需参数后面」的限制了:【「这种写法并不推荐,大家了解一下就好」 「详情移步」】
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
剩余参数
ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如,我们需要实现一个函数 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 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型
//语法
值 as 类型
<类型>值
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 「值 as 类型」。
形如 <Foo> 的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型。所以建议大家在使用类型断言时,统一使用 「值 as 类型」 这样的语法
用途:
- 「将一个联合类型断言为其中一个类型」
- 在不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法
- 需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
// 获取 animal.swim 的时候会报错
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
//如使用类型断言,将 animal 断言成 Fish,就可以解决访问 animal.swim 时报错的问题了
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
// 运行时报错
// (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,
// 将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,
// 故在调用 swim() 时没有编译错误。
// 一旦swim 函数传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时报错。
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`
因此,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
「将一个父类断言为更加具体的子类」
「将任何一个类型断言为
any」「将
any断言为一个具体的类型」「类型断言的限制:」 要使得
A能够被断言为B,只需要A兼容B或B兼容A即可,这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的。
类型别名
类型别名用来给一个类型起个新名字。TypeScript 提供了为类型注解设置别名的便捷语法,你可以使用 type SomeName = someValidTypeAnnotation 来创建别名:
//使用 type 创建类型别名。
type StrOrNum = string | number;
// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';
「类型别名常用于联合类型。」
readonly
前面讲接口的时候有讲到,TypeScript 类型系统允许你在一个接口里使用 readonly 来标记「属性」。它能让你以一种更安全的方式工作(不可预期的改变是很糟糕的):
除了标记属性之外,也可以在 type 里使用 readonly
type Foo = {
readonly bar: number;
readonly bas: number;
};
// 初始化
const foo: Foo = { bar: 123, bas: 456 };
// 不能被改变
foo.bar = 456; // Error: foo.bar 为仅读属性
也可以指定一个类的属性为只读,然后在声明时或者构造函数中初始化它们
class Foo {
readonly bar = 1; // OK
readonly baz: string;
constructor() {
this.baz = 'hello'; // OK
}
}
「readonly 与 const 的区别」
const
- 用于变量;
- 变量不能重新赋值给其他任何事物。
readonly
- 用于属性;
- 用于别名,可以修改属性;
const foo = 123; // 变量
let bar: {
readonly bar: number; // 属性
};