本文由 IMWeb 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。javascript
2018年Stack Overflow Developer的調研(https://insights.stackoverflow.com/survey/2018/)顯示,TypeScript
已經成爲比JavaScript
更受開發者喜好的編程語言了。html
以前我其實對於typescript沒有太多好感,主要是認爲其學習成本比較高,寫起代碼來還要多寫不少類型聲明,而且會受到靜態類型檢查的限制,很不自由,與javascript的設計哲學♂相悖。我相信有不少人也抱持着這樣的想法。前端
然而,最近因爲項目須要,學習並使用了一波typescript,結果。。。java
typescript,顧名思義,就是type + javascript,也就是加上了類型檢查的js。官方對於typescript的介紹也指出,typescript是javascript的超集。純粹的js語法,在typescript中是徹底兼容的。可是反過來,用typescript語法編寫的代碼,卻不能在瀏覽器或者Node環境下直接運行,由於typescript自己並非Ecmascript標準語法。node
不少人堅持javascript而不肯使用typescript的一個很大緣由是認爲javascript的動態性高,基本不須要考慮類型,而使用typescript將會大大削弱編碼的自由度。但實際上,動態性並不老是那麼美好的。至少,如今javascript的動態性帶來了如下三方面的問題:react
代碼可讀性差,維護成本高。webpack
所謂」動態一時爽,重構火葬場「。缺少類型聲明,對於本身很是熟悉的代碼而言,問題不大。可是若是對於新接手或者太長時間沒有接觸的代碼,理解代碼的時候須要自行腦補各類字段與類型,若是不幸項目規模比較龐大,也沒什麼註釋,那麼你的反應大概會是像這樣的:git
有了typescript,每一個變量類型與結構一目瞭然,根本無需自行腦補。搭配編輯器的智能提示,體驗可謂溫馨,媽媽不再用擔憂我拼錯字段名了。es6
缺少類型檢查,低級錯誤出現概率高。github
人的專一力很難一直都保持高度在線狀態,若是沒有類型檢查,很容易出現一些低級錯誤。例如給某個string變量賦值數值,或給對象賦值時候缺乏了某些必要字段,調用函數時漏傳或者錯傳參數等。這些看起來很低級的錯誤,雖然大多數狀況下在自測或者測試階段,都能被驗出來,可是總會浪費你的一些時間去debug。
使用typescript,這種狀況甚至不會發生,一旦你粗心地賦錯值,編輯器當即標紅提示,將bug扼殺在搖籃之中。
類型不肯定,運行時解析器須要進行類型推斷,存在性能問題。
咱們知道javascript是邊解析邊執行的,因爲類型不肯定,因此同一句代碼可能須要被屢次編譯,這就形成性能上的開銷。
雖然typescript如今沒法直接解決性能上的問題,由於typescript最終是編譯成javascript代碼的,可是如今已經有從typescript編譯到WebAssembly的工具了:https://github.com/AssemblyScript/assemblyscript。
好了,若是看完了上面的內容,您仍是表示對於typescript不感興趣,那麼後面的內容就能夠忽略了哈哈哈。。。
typescript中的基礎類型有:
其中,number、string、boolean、object、null、undefined、symbol都是比較簡單的。
例如:
let num: number = 1; // 聲明一個number類型的變量
let str: string = 'string'; // 聲明一個string類型的變量
let bool: boolean = true; // 聲明一個boolean類型的變量
let obj: object = { // 聲明一個object類型的變量
a: 1,
}
let syb: symbol = Symbol(); // 聲明一個symbol類型的變量
null和undefined能夠賦值給除了never的其餘類型。
若是給變量賦予與其聲明類型不兼容的值,就會有報錯提示。
例如:
Array 數組類型
在typescript中,有兩種聲明數組類型的方式。
方式一:
let arr: Array<number> = [1, 2, 3]; // 聲明一個數組類型的變量
方式二:
let arr: number[] = [1, 2, 3]; // 聲明一個數組類型的變量
Tuple 元組類型
元組相似於數組,只不過元組元素的個數和類型都是肯定的。
let tuple: [number, boolean] = [0, false];
any類型
當不知道變量的類型時,能夠先將其設置爲any類型。
設置爲any類型後,至關於告訴typescript編譯器跳過這個變量的檢查,所以能夠訪問、設置這個變量的任何屬性,或者給這個變量賦任何值,編譯器都不會報錯。
let foo: any;
foo.test();
foo = 1;
foo = 'a';
void類型
一般用來聲明沒有返回值的函數的返回值類型。
function foo(): void {
}
never類型
一般用來聲明永遠不會正常返回的函數的返回值類型:
// 返回never的函數必須存在沒法達到的終點
function error(message: string): never {
throw new Error(message);
}
// 返回never的函數必須存在沒法達到的終點
function infiniteLoop(): never {
while (true) {
}
}
never與void的區別即是,void代表函數會正常返回,可是返回值爲空。never表示的是函數永遠不會正常返回,因此不可能有值。
enum 枚舉類型
使用枚舉類型能夠爲一組數值賦予友好的名字。
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
默認狀況下,從0
開始爲元素編號。你也能夠手動的指定成員的數值。例如,咱們將上面的例子改爲從 1
開始編號:
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
或者,所有都採用手動賦值:
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
元素類型也支持字符串類型:
enum Color {Red = 'Red', Green = 'Green', Blue = 'Blue'}
let c: Color = Color.Green;
枚舉類型提供的一個便利是你能夠由枚舉的值獲得它的名字。例如,咱們知道數值爲2,可是不肯定它映射到Color裏的哪一個名字,咱們能夠查找相應的名字:
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 顯示'Green'由於上面代碼裏它的值是2
有點相似其餘強類型語言的強制類型轉換,能夠將一個值斷言成某種類型,編譯器不會進行特殊的數據檢查和結構,因此須要本身確保斷言的準確性。
斷言有兩種形式,一種爲尖括號語法,一種爲as語法。
尖括號語法:
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;
在大部分狀況下,這兩種語法均可以使用,可是在jsx中就只能使用as語法了。
函數類型:
函數類型主要聲明的是參數和返回值的類型。
function sum(a: number, b: number): number {
return a + b;
}
約等於
const sum: (numberA: number, numberB: number) => number = function(a: number, b: number): number {
return a + b;
}
注意到類型定義時參數的名稱不必定要與實際函數的名稱一致,只要類型兼容便可。
可選參數:
函數參數默認都是必填的,咱們也可使用可選參數。
function sum(a: number, b: number, c?: number): number {
return c ? a + b + c : a + b;
}
重載:
javascript自己是個動態語言。javascript裏函數根據傳入不一樣的參數而返回不一樣類型的數據是很常見的。
來看個簡單但沒什麼用的例子:
function doNothing(input: number): number;
function doNothing(input: string): string;
function doNothing(input): any {
return input;
}
console.log(doNothing(123));
console.log(doNothing('123'));
固然也可使用聯合類型,可是編譯器就沒法準確知道返回值的具體類型。
function doNothing(input: number | string): number | string {
return input;
}
console.log(doNothing('123').length); // 錯誤:Property 'length' does not exist on type 'string | number'
若是隻是單純參數的個數不一樣,返回值類型同樣,建議使用可選參數而不是重載。
function sum(a: number, b: number, c?: number) {
return c ? a + b + c : a + b;
}
對於一些複雜的對象,須要經過接口來定義其類型。
interface SquareConfig {
color: string;
width: number;
}
const square: SquareConfig = {
color: 'red', width: 0,
};
可選屬性:
默認狀況下,每一個屬性都是不能爲空的。若是這麼寫,將會有報錯。
interface SquareConfig {
color: string;
width: number;
}
const square: SquareConfig = {
color: 'red',
};// error
能夠將用"?"將width標誌位可選的屬性:
interface SquareConfig {
color: string;
width?: number;
}
const square: SquareConfig = {
color: 'red',
};
只讀屬性:
一些對象屬性只能在對象剛剛建立的時候修改其值。你能夠在屬性名前用 readonly
來指定只讀屬性。
interface Point {
readonly x: number;
readonly y: number;
}
若是在初始化後試圖修改只讀屬性的值,將會報錯。
let p: Point = { x: 10, y: 20 };
p.x = 20; // error
函數類型:
接口除了可以描述對象的結構以外,還能描述函數的類型。
interface SumFunc {
(a: number, b: number): number;
}
let sum: SumFunc;
sum = (numberA: number, numberB: number) => {
return numberA + numberB;
}
能夠看到函數的類型與函數定義時只要參數類型一致便可,參數名不必定要同樣。
可索引類型:
可索引類型,實際就是聲明對象的索引的類型,與對應值的類型。接口支持兩種索引類型,一種是number,一種是string,經過可索引類型能夠聲明一個數組類型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
typescript中的類是javascript中類的超集,因此若是你瞭解es6中的class的語法,也不難理解typescript中class的語法了。
這裏主要說下typescript的class和javascript的class的不一樣之處:
只讀屬性
public、private、protected修飾符
抽象類
實現接口
只讀屬性
相似於接口中的只讀屬性,只能在類實例初始化的時候賦值。
class User {
readonly name: string;
constructor (theName: string) {
this.name = theName;
}
}
let user = new User('Handsome');
user.name = 'Handsomechan'; // 錯誤!name是隻讀的
public、private、protected修飾符:
public修飾符表示屬性是公開的,能夠經過實例去訪問該屬性。類屬性默認都是public屬性。
class Animal {
constructor(public name: string) {}
}
const animal = new Animal('tom');
console.log(animal.name); // 'tom'
注意在類的構造函數參數前加上修飾符是一個語法糖,上面的寫法等價於:
class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
}
private修飾符表示屬性是私有的,只有實例的方法才能訪問該屬性。
class Animal {
getName(): string { return this.name }
constructor(private name: string) {}
}
const animal = new Animal('tom');
console.log(animal.getName()); // 'tom'
console.log(animal.name); // Property 'name' is private and only accessible within class 'Animal'.
protected修飾符表示屬性是保護屬性,只有實例的方法和派生類中的實例方法才能訪問到。
class Animal {
constructor(public name: string, protected age: number) {}
}
class Cat extends Animal {
getAge = ():number => {
return this.age;
}
}
const cat = new Cat('tom', 1);
console.log(cat.getAge()); // 1
抽象類:
抽象類作爲其它派生類的基類使用。它們通常不會直接被實例化。不一樣於接口,抽象類能夠包含成員的實現細節。 abstract
關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。
abstract class Animal {
abstract makeSound(): void; // 抽象方法,必須在派生類中實現
move(): void {
console.log('roaming the earch...');
}
}
class Sheep extends Animal {
makeSound() {
console.log('mie~');
}
}
const animal = new Animal(); // 錯誤,抽象類不能直接實例化
const sheep = new Sheep();
sheep.makeSound();
sheep.move();
實現接口:
類能夠實現一個接口,從而使得類知足這個接口的約束條件。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
泛型在強類型語言中很常見,泛型支持在編寫代碼時候使用類型參數,而沒必要在一開始肯定某種特定的類型。這樣作的緣由有兩個:
有時候沒辦法在代碼被使用以前知道類型。
例如咱們封裝了一個request函數,用來發起http請求,返回請求響應字段。
咱們在實現request函數的時候,其實是不能知道響應字段有哪些內容的,由於這跟特定的請求相關。
因此咱們將類型肯定的任務留給了調用者。
// 簡單封裝了一個request函數
async function request<T>(url: string): Promise<T> {
try {
const result = await fetch(url).then((response) => {
return response.json();
});
return result;
} catch (e) {
console.log('request fail:', e);
throw e;
}
}
async function getUserInfo(userId: string): void {
const userInfo = await request<{
nickName: string;
age: number;
}>(`user_info?id=${userId}`)
console.log(userInfo); // { nickName: 'xx', age: xx }
}
getUserInfo('123');
提升代碼的複用率。
若是對於不一樣類型,代碼的操做都是同樣的,那麼可使用泛型來提升代碼的複用率。
// 獲取數組或者字符串的長度
function getLen<T extends Array<any> | string>(arg: T): number {
return arg ? arg.length : 0;
}
固然,您可能以爲這兩點在javascript中均可以輕易作到,根本不須要泛型。是的,泛型自己是搭配強類型食用更佳的,在弱類型下沒意義。
在typescript中,泛型有幾種打開方式:
泛型函數:
function someFunction<T>(arg: T) : T {
return arg;
}
console.log(someFunction<number>(123)); // 123
泛型類型:
interface
interface UserInfo<T> {
id: T;
age: number;
}
const userInfo: UserInfo<number> = {
id: 123,
age: 23,
}
type
type UserInfo<T> = { // 同上
id: T;
age: number;
}
const userInfo: UserInfo<string> = {
id: '123',
age: 123,
}
泛型類:
class UserInfo<T> {
constructor(private id: T, private age: number) {};
getId(): T {
return this.id;
}
}
咱們也能夠給類型變量加上一些約束。
泛型約束
有時編譯器不能肯定泛型裏面有什麼屬性,就會出現報錯的狀況。
function logLength<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
解決方法是加上泛型約束。
interface TypeWithLength {
length: number,
}
function logLength<T extends TypeWithLength>(arg: T): T {
console.log(arg.length); // ok
return arg;
}
交叉類型:
交叉類型是將多個類型合併爲一個類型。
interface typeA {
a?: number,
}
interface typeB {
b?: number,
}
let value: typeA & typeB = {};
value.a = 1; // ok
value.b = 2; // ok
聯合類型:
聯合類型表示變量屬於聯合類型中的某種類型,使用時須要先斷言一下。
interface TypeA {
a?: number,
}
interface TypeB {
b?: number,
}
const value: TypeA | TypeB = {};
(<TypeA>value).a = 1; // ok
類型別名能夠給一個類型起個新名字。類型別名有時和接口很像,可是能夠做用於原始值,聯合類型,元組以及其它任何你須要手寫的類型。能夠將type
看作存儲類型的特殊類型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
...
is
關鍵字一般組成類型謂詞,做爲函數的返回值。謂詞爲 parameterName is Type
這種形式, parameterName
必須是來自於當前函數簽名裏的一個參數名。
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
這樣的好處是當函數調用後,若是返回true,編譯器會將變量的類型鎖定爲那個具體的類型。
例如:
if (isFish(pet)) {
pet.swim(); // 進入這裏,編譯器認爲pet是Fish類型。
} else {
pet.fly(); // 進入這裏,編譯器認爲pet是Bird類型。
}
keyof
爲索引類型查詢操做符。
interface Person {
name: string;
age: number;
}
type IndexType = keyof Person; // 'name' | 'age'
這樣作的好處是使得編譯器可以檢查到動態屬性的類型。
function pick<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key]);
}
console.log(pick(person, ['name', 'age'])); // [string, number]
爲何須要聲明合併呢?
咱們思考一下,在javascript中,一個對象是否是可能有多重身份。
例如說,一個函數,它能夠做爲一個普通函數執行,它也能夠是一個構造函數。同時,函數自己也是對象,它也能夠有本身的屬性。
因此這注定了typescript中的類型聲明可能存在的複雜性,須要進行聲明的合併。
合併接口
最簡單也最多見的聲明合併類型是接口合併。從根本上說,合併的機制是把雙方的成員放到一個同名的接口裏。
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
接口的非函數的成員應該是惟一的。若是它們不是惟一的,那麼它們必須是相同的類型。若是兩個接口中同時聲明瞭同名的非函數成員且它們的類型不一樣,則編譯器會報錯。
對於函數成員,每一個同名函數聲明都會被當成這個函數的一個重載。同時須要注意,當接口 A
與後來的接口 A
合併時,後面的接口具備更高的優先級。
合併命名空間
Animals
聲明合併示例:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
等同於:
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
命名空間與類和函數和枚舉類型合併
類與命名空間的合併:
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
函數與命名空間的合併:
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam Smith"));
此外,類與枚舉、命名空間與枚舉等合併也是能夠的,這裏再也不話下。
聲明文件一般是以.d.ts結尾的文件。
若是隻有ts、tsx文件,那麼其實不須要聲明文件。聲明文件通常是在用第三方庫的時候纔會用到,由於第三方庫都是js文件,加上聲明文件以後,ts的編譯器才能知道第三庫暴露的方法、屬性的類型。
聲明語法:
declare var
、declare let
、declare const
聲明全局變量
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
declare function
聲明全局方法
declare function jQuery(selector: string): any;
declare class
聲明全局類
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
declare enum
聲明全局枚舉類型
declare enum Directions {
Up,
Down,
Left,
Right
}
declare namespace
聲明(含有子屬性的)全局變量
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
interface
和type
聲明全局類型
interface AjaxSettingsInterface {
method?: 'GET' | 'POST'
data?: any;
}
type AjaxSettingsType = {
method?: 'GET' | 'POST'
data?: any;
}
export
導出變量
在聲明文件中只要用到了export、import就會被視爲模塊聲明文件。模塊聲明文件中的declare關鍵字不能聲明全局變量。
// types/foo/index.d.ts
export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right
}
export interface Options {
data: any;
}
對應的導入和使用模塊應該是這樣:
// src/index.ts
import { name, getName, Animal, Directions, Options } from 'foo';
console.log(name);
let myName = getName();
let cat = new Animal('Tom');
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
let options: Options = {
data: {
name: 'foo'
}
};
export namespace
導出(含有子屬性的)對象
// types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
// src/index.ts
import { foo } from 'foo';
console.log(foo.name);
foo.bar.baz();
export default
ES6 默認導出
// types/foo/index.d.ts
export default function foo(): string;
// src/index.ts
import foo from 'foo';
foo();
export =
commonjs 導出模塊
// 總體導出
module.exports = foo;
// 單個導出
exports.bar = bar;
在 ts 中,針對這種模塊導出,有多種方式能夠導入,第一種方式是 const ... = require
:
// 總體導入
const foo = require('foo');
// 單個導入
const bar = require('foo').bar;
第二種方式是import ... from
,注意針對總體導出,須要使用 import * as
來導入:
// 總體導入
import * as foo from 'foo';
// 單個導入
import { bar } from 'foo';
第三種方式是 import ... require
,這也是 ts 官方推薦的方式:
// 總體導入
import foo = require('foo');
// 單個導入
import bar = foo.bar;
export as namespace
庫聲明全局變量
既能夠經過 <script>
標籤引入,又能夠經過 import
導入的庫,稱爲 UMD 庫。相比於 npm 包的類型聲明文件,咱們須要額外聲明一個全局變量,爲了實現這種方式,ts 提供了一個新語法 export as namespace
。
通常使用 export as namespace
時,都是先有了 npm 包的聲明文件,再基於它添加一條 export as namespace
語句,便可將聲明好的一個變量聲明爲全局變量。
// types/foo/index.d.ts
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
declare global
擴展全局變量
使用 declare global
能夠在 npm 包或者 UMD 庫的聲明文件中擴展全局變量的類型。
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
// src/index.ts
'bar'.prependHello();
declare module
擴展模塊
若是是須要擴展原有模塊的話,須要在類型聲明文件中先引用原有模塊,再使用 declare module
擴展
// types/moment-plugin/index.d.ts
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';
moment.foo();
對於全部的項目,接入ts的第一步就是安裝typescript包,typescript包中包含tsc編譯工具。
npm i typescript -D
新建tsconfig.js
文件,添加編譯配置。
示例:
{
"compilerOptions": {
"noImplicitAny": false,
"target": "es5",
"jsx": "react",
"allowJs": true,
"sourceMap": true,
"outDir": "./out",
"module": "commonjs",
"baseUrl": "./src"
},
"include": ["./src/**/*"],
"exclude": ["./out"]
}
include
表示要編譯的文件所在的位置。
exclude
表示哪些位置的文件夾不須要進行編譯,能夠優化編譯速度。
compilerOptions
中能夠配置編譯選項。其中noImplicitAny表示是否禁止隱式聲明any,默認爲false。
target
表示要將ts代碼轉換成的ECMAScript目標版本。
jsx
可選preserve,react或者react-native。其中preserve表示生成的代碼中保留全部jsx標籤,react-native等同於preserve,react表示將jsx標籤轉換成React.createElement函數調用。
allowJs
表示是否容許編譯js文件,默認爲false。
sourceMap
表示是否生成sourceMap,默認false。
outDir
表示生成的目標文件所在的文件夾。
module
指定生成哪一個模塊系統的代碼。
baseUrl
表示解析非相對模塊名的基準目錄。
詳細配置參數文檔請見:https://www.tslang.cn/docs/handbook/compiler-options.html
有了tsc和tsconfig,實際上就能將ts文件轉換爲js文件了。可是咱們在實際工程的開發中,通常不會直接用tsc,例如在前端項目中,咱們但願能與tsc能和webpack結合起來。在node服務端項目中,咱們但願修改文件以後,可以只編譯修改過的文件,而且重啓服務。下面我將分別介紹前端webpack項目和node項目中接入ts的方法:
前端項目:
好了,很是簡單就完成了webpack項目接入ts。
node項目:
在node項目中,能夠直接使用tsc編譯文件,而後重啓服務,可是這樣在開發階段顯然是很是低效的。
能不能讓node直接執行ts文件呢?這樣結合nodemon
,就能夠很簡單地作到修改文件後自動重啓服務的效果了。有了ts-node
,問題不大!
ts-node
支持直接運行ts文件,就像用node
直接運行js文件同樣。它的原理是對node進行了一層封裝,在require ts模塊的時候,先調用tsc將ts文件編譯成js文件,而後再用node執行。
安裝ts-node
: npm i ts-node -D
運行ts文件:npx ts-node script.ts
因爲ts-node其實是在運行階段對於ts文件進行編譯的,因此通常不在生產環境中直接使用ts-node,而是用tsc直接編譯一遍,就不會有運行時的編譯開銷了。
安裝ts-loader
: npm i ts-loader -D
在webpack.config.js中加入相關的配置項
module.exports = {
mode: "development",
devtool: "inline-source-map", // 生成source-map
entry: "./app.ts",
output: {
filename: "bundle.js"
},
resolve: {
// 添加.ts,.tsx爲可解析的文件後綴
extensions: [".ts", ".tsx", ".js", ".jsx"]
},
module: {
rules: [
// 使用ts-loader解析.ts或者.tsx後綴的文件
{ test: /\.tsx?$/, loader: "ts-loader" }
]
}
};
配置eslint
通過上面的配置以後,若是編譯報錯會在命令行中有提示,而且在vscode中會對出錯的代碼進行標紅。
若是咱們想進一步對於代碼風格進行規範化約束,須要配置eslint。實際上有專門針對typescript的lint工具ts-lint
,可是如今並不推薦使用了,由於爲了統一ts和js的開發體驗,tslint正在逐步地合併到eslint上(https://medium.com/palantir/tslint-in-2019-1a144c2317a9)。
安裝eslint相關依賴
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
。
其中:
eslint
: js代碼檢測工具。
@typescript-eslint/parser
: 將ts代碼解析成ESTree,能夠被eslint所識別。
@typescript-eslint/eslint-plugin
: 提供了typescript相關的eslint規則列表。
配置.eslintrc.js文件
module.exports = {
parser: '@typescript-eslint/parser', // 添加parser
extends: [
'eslint-config-imweb',
'plugin:@typescript-eslint/recommended', // 引入@typescript-eslint/recommended規則列表
],
plugins: ['@typescript-eslint'], // 添加插件
rules: {
'react/sort-comp': 0,
'import/extensions': 0,
'import/order': 0,
'import/prefer-default-export': 0,
'react/no-array-index-key': 1,
},
};
進行了以上的步驟後,發現vscode中仍是沒有將不符合規則的代碼標紅。這裏的緣由是,vscode默認不會對.ts,.tsx後綴的文件進行eslint檢查,須要配置一下。在vscode的setting.json
文件中加入如下配置:
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescriptreact",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
}
]
js項目遷移到ts
對於新的項目,天然不用說,直接開搞。可是對於舊項目,怎麼遷移呢?
首先第一步仍是要先接入typescript,如前文所述。
接下來就有兩種選擇:
若是項目不大,或者下定決心而且有人力重構整個項目,那麼能夠將項目中的.js、.jsx文件的後綴改爲.ts、tsx。不出意外,這時編輯器會瘋狂報錯,耐心地一個個去解決它們吧。
若是項目很龐大,沒法一會兒所有重構,實際上也不妨礙使用ts。
在tsconfig.json
文件中配置allowJs: true
就能夠兼容js。
對於項目中的js文件,有三種處理方式。
不作任何處理。
對文件進行改動時候,順手改爲ts文件重構掉。
給js文件附加.d.ts類型聲明文件,特別是一些通用的函數或者組件,這樣在ts文件中使用到這些函數或者組件時,編輯器會有隻能提示,tsc也會根據聲明文件中的類型進行校驗。
在ts文件中引入npm安裝的模塊,可能會出現報錯,這是由於tsc找不到該npm包中的類型定義文件,由於有些庫是將類型定義文件和源碼分離的。
有三種方式解決這一問題:
若是該庫在@types命名空間下已經有可用的類型定義文件,直接用npm安裝便可,例如
npm i @types/react -D
若是該庫在@types命名空間下沒有可用的類型定義文件,能夠本身寫一個,而後給該庫的做者提個PR。
本地建立一個全局的類型定義文件,例如global.d.ts
。
declare module 'lib' {
export const test: () => void;
}
而後在ts文件中就可使用lib模塊中的test方法了。
import { test } from 'lib';