TypeScript学习笔记
执行下面的命令安装TypeScript:
1 |
npm install -g typescript |
TypeScript支持类型提示,类型提示让代码自动完成更加准确:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 布尔 let isDone: boolean = false; // 数字 let decLiteral: number = 6; let hexLiteral: number = 0xf00d; let binaryLiteral: number = 0b1010; let octalLiteral: number = 0o744; // 字符串 let name: string = "bob"; // 模板字符串 let name: string = `Gene`; let age: number = 37; let sentence: string = `Hello, my name is ${ name }. I'll be ${ age + 1 } years old next month.`; |
1 2 3 4 5 6 7 8 |
// 数组 let list: number[] = [1, 2, 3]; // 数组,泛型语法 let list: Array<number> = [1, 2, 3]; // 元组 let x: [string, number]; x = ['hello', 10]; |
1 2 3 4 5 |
// 枚举 enum Color {Red, Green, Blue} let c: Color = Color.Green; // 默认情况下,枚举值从0开始,可以修改 enum Color {Red = 1, Green, Blue} |
1 2 3 4 5 6 7 8 |
// 不限定的类型 let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; // 你可以假设不限定类型有任何方法,编译器不会检查 notSure.ifItExists(); // 不限定类型数组 let list: any[] = [1, true, "free"]; |
1 2 3 4 5 6 7 |
// Void类型 // 用于表示没有返回值的函数 function warnUser(): void { console.log("This is my warning message"); } // 只能赋值undefined或null let unusable: void = undefined; |
1 2 3 |
// undefined 和 null类型 let u: undefined = undefined; let n: null = null; |
默认情况下 null和 undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给任何类型的变量。但是如果指定了 --strictNullChecks标记,则只能赋值给void或者各自的类型 —— null赋值给null,undefined赋值给undefined。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// never 永远不存在值的类型,例如:总是抛出异常或者不会有返回值的函数或者箭头函数表达式 // 总是抛出异常 function error(message: string): never { throw new Error(message); } // 编译器可以推导出返回值为never类型 function fail() { return error("Something failed"); } // 永远不会返回 function infiniteLoop(): never { while (true) { } } |
表示非基本类型:即非number,string,boolean,symbol,null或undefined。
尖括号语法:
1 2 3 |
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length; |
as语法:
1 2 3 |
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length; |
以一个对象类型为参数,产生字符串或者数字的字面值的联合,这些数字或者字符串来自对象的键:
1 2 3 4 5 6 7 8 9 10 11 12 |
type Point = { x: number; y: number }; type P = keyof Point; // "x" | "y" // 如果目标类型具有字符串或数字的索引签名,则keyof操作符会返回对应的类型(而非字面值)的联合 type Arrayish = { [n: number]: unknown }; type A = keyof Arrayish; // type A = number type Mapish = { [k: string]: boolean }; type M = keyof Mapish; // type M = string | number // 注意这里的M是 string | number的联合,这是因为JavaScript中对象的键,会被强制转换为字符串 // obj[0]和obj["0"]总是一样的 |
后缀的感叹号!表示非空断言操作符,其意义是:
- x! 将从 x 值对应的类型集合中排除 null 和 undefined 的类型。比如 x 可能是 string | undefined,则 x! 类型缩窄为 string
- null!在类型检测器没法正确推断类型情况下,告知编译器值不可能为undefined或者null
因为一个叹号区反,得到的是布尔值,再次取反相当于恢复“原值”(的布尔转型)。
a ?? b仅仅当a为null或者undefined时,返回操作符右侧表达式即b,否则返回a。
TypeScript的核心原则是,对所有结构基于鸭子类型识别,接口用来定义对象具有哪些字段,或者定义函数的规格。
1 2 3 4 5 6 7 8 9 10 |
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj); |
1 2 3 4 |
interface SquareConfig { color?: string; width?: number; } |
1 2 3 4 |
interface Point { readonly x: number; readonly y: number; } |
接口可以用来描述函数的签名:
1 2 3 4 5 6 7 8 9 |
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString); return result > -1; } |
接口中可以声明多个函数签名,这样的接口表示多个重载函数版本的联合:
1 2 3 4 5 6 7 8 9 10 11 |
interface Func1 { (v1: number, v2: number): number; (v1: string, v2: string): string; } const f1: Func1 = (v1, v2): any => { return v1 + v2; }; console.log(f1("1", "2")); console.log(f1(1, 2)); |
可以用来指定数组的值类型、映射的键值类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; // 防止给索引赋值 interface ReadonlyStringArray { readonly [index: number]: string; } interface User { id: string; name: string; [key: string]: any; } let user: User = { id: "alex", name: "Alex" prop1: 1, "prop2": 2 }; |
TypeScript允许字符串、数字作为索引。
接口可以用来限制一个类必须满足的规约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } } interface ClockInterface { currentTime: Date; // 定义方法 setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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; |
接口可以描述一个函数,在限定函数签名的同时,指定函数具有的额外属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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; |
当让接口继承一个类时,它会获得所有类的成员,但是不会获得这些成员的实现。
包括私有、保护成员,都会被继承。这种情况下,只有接口所继承的那个类的子类,才能实现该接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } // 错误:“Image”类型缺少“state”属性。 class Image implements SelectableControl { select() { } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 抽象类 abstract class Greeter { // 静态属性 static cname: string = "Greeter"; // 实例变量 greeting: string; // 可选的访问限定符 private protected public private name: string; // 只读成员 readonly age: number; // 构造函数 constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } let greeter = new Greeter("world"); // 继承 class Dog extends Greeter { greet() { console.log('Woof! Woof!'); // 调用父类的方法 super.greet(); } } |
1 2 |
// 类型提示 let myAdd: (x: number, y: number) => number = function(x: number, y: number): number { return x + y; }; |
1 2 3 4 5 6 7 8 9 |
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName; } let result1 = buildName("Bob"); // works correctly now let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters |
1 2 3 |
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName; } |
当调用该函数的时候,传入undefined或者没有传递,则默认参数生效。
在所有必须参数之后的,带有默认值的参数,都是可选参数。
给予不同的类型提示,即可实现函数重载。
1 2 3 4 5 6 7 8 9 10 11 |
// 函数参数的泛型化 function identity<T>(arg: T): T { return arg; } // 泛型函数的类型提示 let myIdentity: <T>(arg: T) => T = identity; // 显式指定类型参数 let output = identity<string>("myString"); // 自动推导类型参数 let output = identity("myString"); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 某个函数泛型化 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; |
1 2 3 4 5 6 7 8 |
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; }; |
1 2 3 4 5 |
// 规定实际类型必须继承自Lengthwise function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } |
1 2 3 4 5 6 |
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } |
1 2 3 4 |
enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", } |
用于描述已经存在的枚举类型的规格:
1 2 3 4 5 |
declare enum Enum { A = 1, B, C = 2 } |
交叉类型包含所有指定的类型的特征,例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 2 3 4 5 6 7 8 9 10 11 12 |
interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function isFish(pet: Fish | Bird): { return (<Fish>pet).swim !== undefined; } |
可以用于任何类型:
1 2 3 4 5 6 7 8 9 10 11 |
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } } |
类型别名也可以是泛型的:
1 |
type Container<T> = { value: T }; |
甚至可以在类型别名的属性中,引用自己:
1 2 3 4 5 |
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; } |
可以将字符值联合起来,形成类似于枚举的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 { // error! should not pass null or undefined. } } } |
可以为联合类型定义别名:
1 |
type Shape = Square | Rectangle | Circle; |
从TypeScript 1.5开始, “内部模块”称做“命名空间”。 “外部模块”简称为“模块”。
TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。
任何声明都可以通过export关键字来导出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
export interface StringValidator { isAcceptable(s: string): boolean; } export const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } // 先定义再导出 class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export { ZipCodeValidator }; // 导出时指定导出名 export { ZipCodeValidator as mainValidator }; // 重新导出其它模块中的对象 export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator"; // 全部重新导出 export * from "./StringValidator"; // exports interface StringValidator |
1 2 3 4 5 6 7 8 9 10 11 12 |
import { ZipCodeValidator } from "./ZipCodeValidator"; let myValidator = new ZipCodeValidator(); // 导入并重命名 import { ZipCodeValidator as ZCV } from "./ZipCodeValidator"; let myValidator = new ZCV(); // 将整个模块导入到一个变量 import * as validator from "./ZipCodeValidator"; let myValidator = new validator.ZipCodeValidator(); |
每个模块可有最多一个默认导出:
1 2 |
declare let $: JQuery; export default $; |
默认导出在导入时,可以使用任何名字:
1 2 3 |
import jq from "JQuery"; jq("button.continue").html( "Next Step..." ); |
默认导出可以的可以是匿名函数,因为它的名字不重要:
1 2 3 4 5 |
const numberRegexp = /^[0-9]+$/; export default function (s: string) { return s.length === 5 && numberRegexp.test(s); } |
你甚至可以导出一个字面值:
1 |
export default "123"; |
命名空间(以前称为内部模块)可以用来组织代码。
命名空间可以分散在多个文件中:
1 2 3 4 5 |
namespace Validation { export interface StringValidator { isAcceptable(s: string): boolean; } } |
需要利用引用标签,来说明不同文件之间的依赖关系:
1 2 3 4 5 6 7 8 9 |
/// <reference path="Validation.ts" /> namespace Validation { const lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } } |
使用命名空间中定义的对象的语法:
1 2 3 |
let validators: { [s: string]: Validation.StringValidator; } = {}; validators["Letters only"] = new Validation.LettersOnlyValidator(); |
可以为命名空间设置别名,简化访问:
1 2 3 4 5 6 7 8 |
namespace Shapes { export namespace Polygons { export class Triangle { } export class Square { } } } import polygons = Shapes.Polygons; |
JSX是一种源于React的类似于XML的语法,可以嵌入在JavaScript的源码中。
要在TypeScript中使用JSX,你需要:
- 给文件一个.tsx扩展名
- 启用jsx选项,利用--jsx命令行标记或者在tsconfig.json中配置
TypeScript具有三种JSX模式:preserve,react和react-native。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响:
- preserve:生成代码中会保留JSX以供后续的转换操作(例如Babel)使用
- react:生成React.createElement。注意React这个标识符是写死的,不能被占用
- react-native:相当于preserve,它也保留了所有的JSX,但是输出文件的扩展名是.js
由于尖括号风格的断言 var foo = <foo>bar;和JSX语法冲突,因而在.tsx文件中,这种风格的断言不被支持,必须使用as操作符。
要使用TypeScript编写Node.js程序,可以借助ts-node。ts-node是Node.js的TypeScript执行引擎,它能够JIT的将TypeScript编译为JavaScript,你不需要预先编译就可以在Node.js运行时上执行TypeScript。
ts-node的特性包括:
- 为stacktrace自动生成sourcemap
- 自动解析tsconfig.json
- 自动设置匹配Node.js版本的默认值
- 类型检查
- REPL
- 原生ESM模块加载器支持
- 支持使用第三方transpiler
- 和debugger集成
1 2 3 4 5 6 7 8 9 10 |
# Locally in your project. npm install -D typescript npm install -D ts-node # Or globally with TypeScript. npm install -g typescript npm install -g ts-node # Depending on configuration, you may also need these npm install -D tslib @types/node |
1 2 3 4 5 6 7 8 9 10 11 |
# 执行脚本 ts-node script.ts # 启动解释器 ts-node # 执行命令行提供的脚本 ts-node -e 'console.log("Hello, world!")' # 执行并打印 ts-node -p -e '"Hello, world!"' |
1 2 3 |
#!/usr/bin/env ts-node console.log("Hello, world!") |
可以通过下面的代码请求ts-node模块并注册它的loader,此loader会影响后续的requires。ts-node的工作原理就是挂钩到Node的模块加载API。
1 |
require('ts-node').register({ /* options */ }) |
或者使用命令行参数:
1 2 |
node -r ts-node/register node -r ts-node/register/transpile-only |
ts-node会自动查找并加载tsconfig.json文件。命令行选项 --skipProject可以跳过当前项目的tsconfig.json, --project则用于明确指定项目配置文件的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ // This is an alias to @tsconfig/node12: https://github.com/tsconfig/bases "extends": "ts-node/node12/tsconfig.json", // 大部分配置在此 "ts-node": { // 如果要跳过类型检查,去除下面这一行 "transpileOnly": true, "files": true, "compilerOptions": { // 覆盖下面的编译选项 } }, "compilerOptions": { // typescript编译选项 } } |
TypeScript基本上都会使用import语法,但是可以在执行期间转换为CommonJS的require()或者保持ESM风格的import不变。
如果要转换为CommonJS风格,在package.json配置:
1 2 3 4 |
{ // This can be omitted; commonjs is the default "type": "commonjs" } |
在tsconfig.json中配置:
1 2 3 4 5 |
{ "compilerOptions": { "module": "CommonJS" } } |
如果需要为tsc,webpack等工具保留"module": "ESNext",你可以:
1 2 3 4 5 6 7 8 9 10 |
{ "compilerOptions": { "module": "ESNext" }, "ts-node": { "compilerOptions": { "module": "CommonJS" } } } |
NodeJS的原生ESM加载器钩子还处于试验阶段,可能改变。ts-node的ESM支持会尽量保持稳定,但是它依赖于底层的Node API。
你需要在package.json中设置:
1 2 3 |
{ "type": "module" } |
在tsconfig.json中设置:
1 2 3 4 5 6 7 8 9 |
{ "compilerOptions": { "module": "ESNext" // or ES2015, ES2020 }, "ts-node": { // Tell ts-node CLI to install the --loader automatically, explained below "esm": true } } |
同时,你还需要确保--loader被传递给Node。如果使用ts-node命令行,会自动传递:
1 2 3 |
# 等价于上面配置文件中的"esm": true ts-node --esm ts-node-esm |
如果你直接使用Node的命令行,则需要手工传递:
1 2 3 |
node --loader ts-node/esm ./index.ts # 或者 NODE_OPTIONS="--loader ts-node/esm" node ./index.ts |
安装ts-node:
1 |
npm install --save-dev ts-node |
创建Node.js运行配置,增加Node Parameters: --require ts-node/register。
使用ESM时,参考上文,即增加Node Parameters: --loader ts-node/esm。
Leave a Reply