TypeScript 是微软开发的JavaScript 的超集,扩展了 JavaScript 的语法,现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
典型 TypeScript 工作流程
下图包含 3 个 ts 文件:a.ts、b.ts 和 c.ts。这些文件将被 TypeScript 编译器,根据配置的编译选项编译成 3 个 js 文件,即 a.js、b.js 和 c.js。对于大多数使用 TypeScript 开发的 Web 项目,我们还会对编译生成的 js 文件进行打包处理,然后在进行部署。
一、基础类型
any、number、string、boolean、数组、元组、枚举、void、null、undefined、never
数据类型 | 关键字 | 描述 |
任意类型 | any | 声明为 any 的变量可以赋予任意类型的值。 有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 在对现有代码进行改写的时候, |
数字类型 | number | 双精度 64 位浮点值。它可以用来表示整数和分数。 |
字符串类型 | string | 一个字符系列,使用单引号(')或双引号(")来表示字符串类型。还可以使用模板字符串。 |
布尔类型 | boolean | 表示逻辑值:true 和 false。 |
数组类型 | 无 | 有两种方式可以声明数组。 |
元组 | 无 | 元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。 |
枚举 | enum | 枚举类型用于定义数值集合。使用枚举类型可以为一组数值赋予友好的名字。 默认情况下,从 或者,全部都采用手动赋值: 枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字: |
void | void | 某种程度上来说, 声明一个 |
null和undefined | null / undefined | 默认情况下 然而,当指定了 |
never | never | never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
|
let notSure: any = 4;notSure = "maybe a string instead";notSure = false; // okay, definitely a boolean |
二、类型断言
TypeScript中类型断言(Type Assertion
)可以用来手动指定一个值的类型,用来覆盖TS中的推断。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";let strLength: number = (<string>someValue).length; |
另一个为as
语法:
let someValue: any = "this is a string";let strLength: number = (someValue as string).length; |
TypeScript 是怎么确定单个断言是否足够
当 S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 any。
三、类型推断
当类型没有给出时,TypeScript 编译器利用类型推断来推断类型。
如果由于缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 any 类型。
var num = 2; // 类型推断为 numberconsole.log("num 变量的值为 "+num); num = "12"; // 编译错误 |
- 第一行代码声明了变量 num 并=设置初始值为 2。 注意变量声明没有指定类型。因此,程序使用类型推断来确定变量的数据类型,第一次赋值为 2,num 设置为 number 类型。
- 第三行代码,当再次为变量设置字符串类型的值时,这时编译会错误。因为变量已经设置为了 number 类型。
四、接口
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
interface LabelValue { label: string;}function printLabel(labelObject: LabelValue) { console.log(labelObj.label);}let myObj = {size: 10, label: "Size 10 Object"};printLabel(myObj); |
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。
interface SquareConfig { color?: string; width?: number;} |
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,不小心将 createSquare
里的color
属性名拼错,就会得到一个错误提示:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.clor) { // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.clor; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare;}let mySquare = createSquare({color: "black"}); |
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 可以在属性名前用 readonly
来指定只读属性:
interface Point { readonly x: number; readonly y: number;}let p1: Point = { x: 10, y: 20 };p1.x = 5; // error! |
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! |
上面代码的最后一行,可以看到就算把整个ReadonlyArray
赋值到一个普通数组也是不可以的。 但是可以用类型断言重写:
a = ro as number[]; |
额外的属性检查
对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,就会得到一个错误。
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { // ...}let mySquare = createSquare({ colour: "red", width: 100 });// error: 'colour' not expected in type 'SquareConfig' |
绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); |
还可以添加一个字符串索引签名:
interface SquareConfig { color?: string; width?: number; [propName: string]: any;} |
还有最后一种跳过这些检查的方式,就是将这个对象赋值给一个另一个变量: 因为 squareOptions
不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 };let mySquare = createSquare(squareOptions); |
函数类型
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
类型变量。
可索引的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
interface StringArray { [index: number]: string;}let myArray: StringArray;myArray = ["Bob", "Fred"];let myStr: string = myArray[0]; |
上面例子定义了StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引StringArray
时会得到string
类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal { name: string;}class Dog extends Animal { breed: string;}// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!interface NotOkay { [x: number]: Animal; [x: string]: Dog;} |
interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number类型 name: string // 错误,`name`的类型与索引类型返回值的类型不匹配} |
最后,可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray { readonly [index: number]: string;} |
类类型
TypeScript能够强制一个类去符合某种契约。
interface ClockInterface { currentTime: Date; setTime(d: Date);}class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { }} |
注:当一个类实现了一个接口时,只对其实例部分进行类型检查。constructor存在于类的静态部分,所以不在检查的范围内。
继承接口
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape { color: string;}interface Square extends Shape { sideLength: number;} |
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape { color: string;}interface PenStroke { penWidth: number;}interface Square extends Shape, PenStroke { sideLength: number;} |
混合类型
如果希望一个对象可以同时做为函数和对象使用,并带有额外的属性,就需要用到混合类型
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; |
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
class Control { private state: any;}interface SelectableControl extends Control { select(): void;}class Button extends Control implements SelectableControl { select() { }}class TextBox extends Control { select() { }}// 错误:“Image”类型缺少“state”属性。class Image implements SelectableControl { select() { }}class Location {} |
五、函数
可选参数
在 TypeScript 函数里,如果我们定义了参数,则我们必须传入这些参数,除非将这些参数设置为可选,可选参数使用问号标识 ?。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName;} let result1 = buildName("Bob"); // 错误,缺少参数let result2 = buildName("Bob", "Adams", "Sr."); // 错误,参数太多了let result3 = buildName("Bob", "Adams"); // 正确 |
可选参数必须跟在必需参数后面。 如果上例我们想让 firstName 是可选的,lastName 必选,那么就要调整它们的位置,把 firstName 放在后面。如果都是可选参数就没关系。
函数重载
JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。这个时候就需要用到重载。
定义函数重载需要定义重载签名和一个实现签名。
重载签名定义函数的形参和返回类型,没有函数体。一个函数可以有多个重载签名(不可调用)
let suits = ["hearts", "spades", "clubs", "diamonds"];// 定义重载签名function greet(person: string): string;function greet(persons: string[]): string[];// 定义实现签名function greet(person: unknown): unknown { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } throw new Error('Unable to greet');} |
六、泛型
可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
例:
function identity(arg: any): any { return arg;}//identity函数会返回任何传入它的值。 |
使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T { return arg;} |
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity
函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string' |
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string' |
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。
使用泛型变量
可以把泛型参数T当作类型变量来使用
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg;} |
泛型类型
interface GenericIdentityFn { <T>(arg: T): T;}function identity<T>(arg: T): T { return arg;}let myIdentity: GenericIdentityFn = identity; |
还可以把泛型参数当作整个接口的一个参数,这类接口称为泛型接口
interface GenericIdentityFn<T> { (arg: T): T;}function identity<T>(arg: T): T { return arg;}let myIdentity: GenericIdentityFn<number> = identity; |
注:第二个例子中myIdentity的入参只能为number,而第一个例子中myIdentity的入参可以为任意类型
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>
)括起泛型类型,跟在类名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber<number>();myGenericNumber.zeroValue = 0;myGenericNumber.add = function(x, y) { return x + y; }; |
泛型约束
可以定义一个接口来描述约束条件,使得泛型参数T需要满足特定条件
interface Lengthwise { length: number;}function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg;}loggingIdentity(3); // Error, number doesn't have a .length property |
七、高级类型
联合类型
联合类型(Union Types)可以通过管道(|)将变量设置多种类型,赋值时可以根据设置的类型来赋值。
interface Bird { fly(); layEggs();}interface Fish { swim(); layEggs();}function getSmallPet(): Fish | Bird { // ...}let pet = getSmallPet();pet.layEggs(); // okaypet.swim(); // errors |
交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和 Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
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. typeof
在 TypeScript 中,typeof
操作符可以用来获取一个变量声明或对象的类型。
interface Person { name: string; age: number;} const sem: Person = { name: 'semlinker', age: 30 };type Sem= typeof sem; // -> Person function toArray(x: number): Array<number> { return [x];} type Func = typeof toArray; // -> (x: number) => number[] |
2. keyof
keyof
操作符可以用来获取一个对象中的所有 key 值:
interface Person { name: string; age: number;} type K1 = keyof Person; // "name" | "age"type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" type K3 = keyof { [x: string]: Person }; // string | number |
3. in
in
用来遍历枚举类型:
type Keys = "a" | "b" | "c" type Obj = { [p in Keys]: any} // -> { a: any, b: any, c: any } |
4. infer
在条件类型语句中,可以用 infer
声明一个类型变量并且对它进行使用:
type ReturnType<T> = T extends ( ...args: any[]) => infer R ? R : any; |
5. Partial
Partial<T>
的作用就是将某个类型里的属性全部变为可选项 ?
。
type Partial<T> = { [P in keyof T]?: T[P];}; |
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值。中间的 ?
号,用于将所有属性变为可选。
示例:
interface Todo { title: string; description: string;} function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { return { ...todo, ...fieldsToUpdate };} const todo1 = { title: "organize desk", description: "clear clutter",}; const todo2 = updateTodo(todo1, { description: "throw out trash",}); |
在上面的 updateTodo
方法中,我们利用 Partial<T>
工具类型,定义 fieldsToUpdate
的类型为 Partial<Todo>
,即:
{ title?: string | undefined; description?: string | undefined;} |