将带你了解 TypeScript 基础知识,感受四季:
本系列其他文章:
万物起源:从 JavaScript 到 TypeScript
渐进增强:入门 TypeScript 编写 React 应用
TypeScript 支持与 JavaScript 几乎相同的数据类型,只不过它会比 JavaScript 更为丰富,详情参考: http://www.typescriptlang.org/docs/handbook/basic-types.html。
但在 TypeScript 的世界中因为推导的关系,我们可以不必明确给予类型,因为编译器会通过值来推导类型,大部分情况下这都是有用的,不过既然这是一个静态类型系统,我们更期望在书写的过程中能有非常明确的类型。
let is: boolean = true;let isN: number = 110;let isS: string = "";let isA: Array = [];let isNull: null = null;let isUndefined: undefined = undefined;
上述的类型基本上 JavaScript 中都能找到对应的含义:
其中数组还有一种简写,如:let isA: any[] = []
,除此之外,TypeScript 在基本类型上扩充了一些其他非常有用的类型:
let x: [string, number] = ["hello", 110];enum Color { Red, Green, Blue}let Ans: any = 4;let Ans: any = "4";let unusable: void = undefined;const create = (o: object | null) => {}create({ prop: 0 });function error(msg: string) never { throw new Error(msg);}
多数情况下,元组
的使用倒是可以在函数返回多个值时有用,一般情况下,我们可能会使用不到它。
而 Never
来表示从未发生的值的类型,当你的函数在执行过程抛出了错误,并未到达返回时,即可使用。
Any
的情况是在未知类型的时候使用,不过程序一般都有比较明确脉络走向,我们可以使用泛型来代替 Any 去处理这样的状况,至于 Void
一般也是用于定义函数没有返回值时所用。
object
则是一种非基本类型的类型(这里的基本类型是值 JavaScript 中定义的基本类型),即任何不是number,string,boolean,symbol,null 或者 undefined 的类型。
对于类型断言来说有两种方式可以处理:
let someValue: any = "this is a string";let strLength: number = (someValue).length;let strLength2: number = (someValue as string).length;
<>
或 as
关键字,这种情况是除非你非常明确这里的类型,在编译时编译器会进行类型断言。
在 TypeScript 扩充的这些基本类型中,比较常用的还是枚举,它不仅可以使用数字枚举,也可以字符串枚举,和其他语言中的枚举一样,它的值都有着明确特殊含义的,与你的编程逻辑密不可分。
enum Color { Red = "red", Green = "green"}const typed = document.getElementById("typed");if (typed) { typed.style.background = Color.Red;}
在上述的文字中,其实你应该可以发现存在 let
关键字,接下来它与我想说的变量声明有非常大的关系。
在以往的 JavaScript 中 var
是我们来定义一个变量的开始,es2015 普及之后,let
和 const
几乎就成了我们编写代码时仅选择的两个变量声明方式,虽然某些特殊的场景下,我们也会使用到 var
。说到变量的声明,其实要说到作用域的概念,这有一些比较枯燥的理论知识,所以简单的来说,不管是 let
还是 const
它的作用域都是块作用域,一个是可以在块作用域中随意赋值的 let
,一个是一旦赋值就不可改变的 const
。何谓块
,简单的理解可以为只要是 {}
包裹的区域,它就是一个块,在这个块中变量的声明是有一定规则的,最主要的规则就是:在块中声明的变量是无法被块作用域外的变量所访问,更多的理论知识可参考http://www.typescriptlang.org/docs/handbook/variable-declarations.html 。
说到这里,其实我想说一说我们在写 es2015 时非常有用的一个特性:解构,在 TypeScript 中解构也是非常有用的一个特性:
let input = [1,2]let [f, s] = input; // 数组解构
function a([f,s]:[number,number]){} // 用于函数的数组解构a([1,2])
let input = [1,2,3,4];let [f, ...s] = input;//s [2,3,4] ...展开语法
let o = { a: 1, b: 2}let { a, b} = o; // 对象解构
正常情况下,我们非常希望在对象的解构过程中给予一些类型,简单来说我们应该通过一个 interface
来定义一个对象,上述的情况也可以将类型赋值上去,只是代码看起来不是很满意,如:
let { a, b }: {a:number, b: number } = o;
枚举允许我们定义一组常量来表达和记录更明显的意图。
我们知道通常情况下网络会有三个状态,我们使用枚举来定义,如:
enum Network { OK, ERROR, TIMEOUT}
在这里编译器会默认从 0
开始自动递增,这种情况对于我们不是很关心枚举的值时非常有用,但网络状态其实可以赋予一组更有意义的值,如:
enum Network { OK = 200, ERROR = 400, TIMEOUT = 408}
很明显,我们可以用另外一个例子来举一反三,当我们需要对一个状态来表达 No
或 Yes
时:
enum Status { Yes, No}
我们也可以很简单的使用枚举,如:
const Http = (): Network => { return Network.OK;}
有时候当数值无法表达我们的意图时,也可以选择字符串枚举来处理这个问题,只是字符串枚举将没有自增的行为,不过对于序列化来说明显会更有意义。
enum Color { red = "red", blue = "blue"}const color = `color:${Color.red};`;
TypeScript 给予枚举的能力并不限于此,有趣的是计算枚举并不是很常见,当然如果你愿意,你完全可以将它变成计算枚举,如:
enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length}
另外枚举成员还可以是类型,这些特殊的语义可以很好的表达:某些成员只能拥有枚举成员的值,如:
enum ShapeKind { Circle, Square,}interface Circle { kind: ShapeKind.Circle; radius: number;}interface Square { kind: ShapeKind.Square; sideLength: number;}let c: Circle = { kind: ShapeKind.Square, radius: 100,}
你可以非常明确看到一行错误:
Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
枚举的意义其实在于我们组织代码时对一些标识能有很明确的表达出意图,这种感觉比在 JavaScript 中要舒服很多。
函数作为 JavaScript 世界中的一等公民,在描述如何执行操作中起到了关键作用。 TypeScript 在此之上为函数添加一些新的功能,让其更好用便成为了其意义的开始。
function addNums(x: number, y: number): number { return x + y;}const addNums1 = (x: number, y: number) => x + y;const addNums2 = function(x: number, y: number){ return x + y;}
对于函数而言我们可以为参数,返回值添加其应有的类型,这对于函数的执行来说有很明确的意义,只是如果我们要输入完整的类型,它的长度有时会变的可怕,我更希望这是能可控的:
type AddFunction = (x: number, y: number) => number;const d = (add: AddFunction): number => { return add(1,2);}
接下来我们要去看一看 TypeScript 为其添加的些许新功能,除了类型之外。
在 TypeScript 中函数需要为每个参数定义类型,但这并不意味着不能被赋予未定义,有时候可选还是非常有用的:
const optional = (x?: number): number | undefined => { if (x){ return x; }}optional()optional(1)
当然,如果你很明确传入参数的意图也可以使用 !
跳过编译器对它的检查。
在 TypeScript 中我们还可以为参数设置一个默认值,因为如果使用者没有传递参数时,未定义并不是我们想要的意义:
const defaultParams = (x = 1): number => { return x;}
如果有时候你想将多个参数作为一个 group 来使用,这个时候 rest 参数才会有明显的意义:
const restParams = (x: number, ...options: number[]) => {}restParams(1);restParams(1,2,3,4,5);
JavaScript 中并没有关于重载的特性,但如果对于不同的参数返回不同的结果,这确却是有意义的:
function overloads(x: number): number;function overloads(x: string): string;function overloads(x: any): any { if (typeof x === "number") { return 0; } if (typeof x === "string") { return "0" }}overloads(1);overloads("1");
由于 TypeScript 是 JavaScript 的超集,因此 this
的行为和 JavaScript 一样,但 TypeScript 为你提供了几种方式来捕获不正确的用法:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
由于我们手动调用了 createCardPicker
,这里的 this
在严格模式中将是 undefined
而不是 window
,当你将配置项中的 noImplicitThis
设置为 true 时,编译器在编译时会为你给出一份警告:
'this' implicitly has type 'any' because it does not have a type annotation.An outer value of 'this' is shadowed by this container.
通常对于此类情况的 fix 非常有参考价值,修复它也非常容易:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
当你使用 call
来改变 this
时,我们也应该期望给它一个明确的类型,有时它真的非常有用,比如:
class Call{ say(){ }}const call = new Call();const func = function(this: Call){ this.say();}func.call(call);
如果你时常使用 addClickListener
来监听事件,那么应该知道使用第一个参数 this:void
来描述 func
的 this
,这非常有意义。
类型检查属于 TypeScript 的核心原则之一,在 TypeScript 的世界里接口充当了命名这些类型的角色。
先让我们来看一个例子:
const printLabel = (o: { label: string, size: number }) => { //}printLabel({size: 10, label: ""});
多数情况下传递的对象参数可能会有多个属性,这也意味着当明确属性变为不可能时,它的长度将会特别可怕,要知道 type
和 接口的唯一区别就是接口可以被继承,接下来我们可以稍微改动一下:
interface ILable { label: string; size: number;}const label = (o: ILable) => {}label({ label: "", size: 10});
有时候并非所有的属性都是必须的,因此我们需要让属性变成可选:
interface ILable { label: string; size?: number;}const label = (o: ILable) => {}label({ label: "", size: 10});label({ label: ""})
如果有些属性只能在对象首次创建时对其赋值,我们可以将其变成只读:
interface IPoint { readonly x: number; readonly y: number;}let point: IPoint = { x: 1, y: 10};
如果再赋值会得到一行错误:
point.x = 1;
Cannot assign to 'x' because it is a read-only property.ts
当我们需要给予某些函数类型时,我们就可以如下:
interface IFunction { (): void;}let funcs: IFunction = () => {}
正如我们用接口来描述函数类型一样,我们也可以使用接口来描述可索引的类型:
interface IArray { [index: number]: string;}let arr: IArray = ["1", "2"];arr[0];
支持索引签名的类型有两种:
我们可以在接口中描述一个方法或属性,然后在类里实现它:
interface IClass { date: Date; getTime: () => number;}class Time implements IClass { date = new Date(); getTime(){ return this.date.getTime(); } constructor(){ }}const time = new Time();
和类一样,接口也是可以互相继承的,并且一个接口可以继承多个接口:
interface IA { a: string;}interface IB extends IA { b: number;}let exten: IB = { a: "1", b: 1}
先前我们提过接口是 TypeScript 来描述类型的核心原则之一,因此接口可以将多种类型混合起来:
interface IHybrid { name: string; age: number; add: () => void;}const hybrid: IHybrid = { name: "icepy", age: 0, add: function(){ }}
当接口继承一个类时它将会继承类的所有成员但不包括实现:
class Base { public type: string; constructor(type: string){ this.type = type; } logType(){ return this.type; }}interface IButton extends Base {}class Button implements IButton { type = "button" constructor(){} logType(){ return this.type; }}
在没有出现 es2015 之前,在 JavaScript 中,我们都使用函数和原型来完成一个类的定义,但这对于熟悉其他面向对象的程序员(Java)来说非常的艰难,于是 es2015 将我们复杂的面向对象编程简化了不少,反而 TypeScript 对于它还有一些增强的扩展,我们可以一直使用它而不必等待下一版本的 JavaScript 标准实现,并且这和标准非常的类似。
让我们来看一个简单的基于类的例子:
class World { public country: string; private max: number; constructor(country: string){ this.country = country; this.max = 110; } output() { return this.country; }}class Country extends World { constructor(country: string){ super(country); }}
在这个范例中,我们既定义了类也完成了继承,看起来是不是和 es2015 几乎一样呢?当然它也有一些与 es2015 的标准稍有不同,因为 TypeScript 增强了关于 public
private
之类的一些定义。
private
类似,唯一的不同是它可以在派生类中自由的访问class StaticClass { static orig = ""; static origFun = () => {}}
如果你使用过 Object.defineProperty
那么一定理解如 getter
setter
钩子的作用,在 TypeScript 中类也定义了非常类似的东西,举个例子:
class Employee { private _fullName: string; constructor(){ this._fullName = ""; } get fullName(): string { return this._fullName; } set fullName(newValue: string) { this._fullName = newValue; }}const emp = new Employee();emp.fullName = "";
比对一下编译之后的代码:
var Employee = /** @class */ (function () { function Employee() { this._fullName = ""; } Object.defineProperty(Employee.prototype, "fullName", { get: function () { return this._fullName; }, set: function (newValue) { this._fullName = newValue; }, enumerable: true, configurable: true }); return Employee;}());var emp = new Employee();emp.fullName = "";
是不是发现了 Object.defineProperty
? 在使用存取器的过程中,唯一要注意的是如果只定义了 get
钩子而没有定义 set
钩子的话,这个属性将是 readonly
。
这个概念几乎在 JavaScript 从未存在过,如果有其他面向对象编程经验的程序应该会比较明白,说一个非常简单类似的设计,如iOS 中的 protocol:
abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必须在派生类中实现}class AccountingDepartment extends Department { constructor() { super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); }}
在软件工程领域,我们不仅要创建定义一致良好的API,也需要同时考虑重用性,泛型就给予了这样的灵活性又不失优雅。
让我们先来创建一个简单的泛型函数:
function r(args: T): T { return args;}r("icepy");r(100)r(true)
当我们不知道返回类型时,泛型函数就解决了这样的问题,虽然这看起来和 Any
非常的类似。
由于我们定义的是泛型函数,因此它并不会丢失类型,反而会根据我们传入的参数类型而返回一个类型,这也意味着我们可以将它适用于多个类型。
type GenericsR = (args: T) => T;function r(args: T): T { return args;}const _r: GenericsR = r;
创建类型,其实仅是从编程风格上来说更统一和方便使用。
泛型类其实看上去和定义一个泛型函数很类似,如:
class GenericsClass{ public add?: (x: T, y: T) => T;}const cls = new GenericsClass();cls.add = (x: number, y: number): number => { return x + y;}
从英译的文字来看 高级类型
并未有我们想象的那么复杂,这只是对于我们日常的编程生活中的一些补充,某些场景下,这些类型会为你的编程范式带来便捷。
当我们从最初的 mixins
中获取收益时,你就需要用到如下的一个类型了:
function extend(first: T, second: U): T & U { let result = {}; for (let id in first) { (result)[id] = (first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (result)[id] = (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();
如果你使用过 vue 的话,应该体验过 mixins
带来的便捷性。
当我们写了一个函数,它的参数预期可能是 number
也可能是 string
,除了泛型的方式之外,我们也可以使用这样的方式来处理这个问题:
function sumStr(x: number | string): string { if (typeof x === "number") { return `${x}`; } if (typeof x === "string") { return `${x}`; } throw new Error("not number or string");}
从符号上来说 x
可能是 number
也可能是 string
,如果是其他类型,就抛出一个错误。
我们再来看一个稍微复杂一些的范例:
class A { log(){ console.log("A"); }}class B { logg(){ console.log("B"); }}function getx(): A | B { return new A();}const x = getx();(x).log();
如果当你返回两个不同的类型时,为了让这段代码可以工作,我们需要断言一个明确的类型,然后才开始工作。
与 JavaScript 一样,我们都使用 typeof
和 instanceof
来做类型保护,用于判断当一个变量是未知时的类型判断。这些预期的行为与 JavaScript 没有任何区别,因此我们应该去更好的理解 instanceof
来处理对象。
大部分情况下,如果没有使用接口来定义类型,我们为会比较杂乱的类型定义一个别名,这对于程序的可阅读性有较高的提升,如:
type info = { name: string; age: number; x: string;}let a: info = { name: "", age: 0, x: ""}let b: { name: string; age: number; x: string;} = { name: "", age: 0, x: ""}
你能所预期的,如果没有别名,这些不是接口定义的类型,随着属性的增加会恐怖到什么地步。既然我们提到了接口,那么有一些不一样的地方,那是它与别名的区别,别名不能像接口那样被继承和被实现。
如果一个对象具备 Symbol.iterator
的实现,则认为该对象是可迭代的,一些内置的类型如:Array
,Map
,Set
等。
for..of
会遍历可迭代对象,调用对象上的 Symbol.iterator
方法,下面是在数组上使用 for..of
的简单例子:
let someArray = [1, "string", false];for (let entry of someArray) { console.log(entry); // 1, "string", false}
如果生成的代码目标是 ES5
或 ES3
,迭代器只允许在 Array
上使用,如果是其他对象,除非已经实现了 Symbol.iterator
方法。
如果是 ECMAScript 2015
时,编译器会生成对应引擎的内置实现。
从 es2015 开始 JavaScript 有了自己的模块,TypeScript 也遵循了这样的定义,其实在 TypeScript 还有一个命名空间的概念,这样的概念应该说从历史遵循而来,上了年纪的前端程序员应该会对YUI有一些印象,当年的前端对于命名空间的运用,这是最出色的一款框架。不过,今天我们将讲一讲 TypeScript 中的模块,以及另外一些模块规范的第三方包,该如何在 TypeScript 中引用。
对于模块而言它自身是运行在一个局部的作用域中的,这一点非常重要,这也意味着在一个模块中的变量,函数,类等,除非你导出,外部程序是无法使用的,因此 TypeScript 也遵循了 import
和 export
的机制,反之如果一个文件内没有这两个关键字存在的话,那么它的内容将会被视为全局。
导出一个常量:
export const D = "";
导入一个常量:
import { D } from "xx";
导出关键字和导入关键字都适用于重命名,如:
import { D as Dell } from "xx"const C = "";export { C as D }
我们也可以将所有的导出都导入到一个变量中,如:
export const A = "";export const B = "";
import * as CONST from "xx";
每一个模块都可以使用一个默认的导出,如:
export default function reducers () {}
import reducers from "xx";
如果你使用过 React
那么就应该对 jsx
非常的熟悉,这是一种嵌入式类似XML语法的可转换成 JavaScript 的设计实现,它非常的灵活在 JavaScript 中,我个人非常的喜欢 jsx
,完全符合我自己的口味,菜好不好吃,只有自己去试试才知道。
TypeScript 也对于 jsx
有三种模式的支持,我们可以在 tscnfig.json
文件中找到:
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
preserve
react-native
react
要启用 jsx
在 TypeScript 的世界中需要做两件事情:
.tsx
后缀的文件tsconfig.json
配置文件中启用 jsx
选项这三种模式下,各有不同。
preserve
模式下对代码的编译会保留 jsx
格式,并输出一份 .jsx
后缀的文件。react
模式下对代码的编译是直接转换成 React.createElement
,并输出一份 .js
后缀的文件。react-native
模式下对代码的编译会保留 jsx
格式,并输出一份 .js
后缀的文件。其他的使用方式几乎一样,并未有过多需要注意的地方,目前 TypeScript 3.0 已经支持了 defaultProps
,这也意味着我们可以很方便的定义默认值,不必像以前那样搞的很复杂,这一点上来说是新版本的 TypeScript 对 react
更好的支持。
类型检查还是会和正常的 TypeScript 一样(.ts
后缀的文件名),因此我们不必过于担心。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5cc08ab1fe554b79722e510b
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。