1. 程式人生 > >TS引用JS模組

TS引用JS模組

為TypeScript引用的JS寫宣告檔案

寫TypeScript宣告檔案的時候會有三個困惑,一個是宣告檔案是什麼?一個是宣告檔案怎麼寫?還有一個是TS依據什麼規則找到我們的宣告檔案或者說模組。

第一個問題:按照我的理解宣告檔案就是告訴TS編譯器有哪些模組?有哪些變數?變數分別是什麼型別?所以如果說原本就是TS寫的程式碼這些都是具有的,但是JS寫的程式碼就不會有這些,因為這些強型別是TS對JS的擴充套件,JS沒有這個特性。

第二個問題:這個需要看官方的文件(下文也簡單舉例一些例子),並且要仔細看,如果語法錯誤也會帶來很多困擾。

第三個問題:這個涉及到TS的模組解析,詳情請看 官文–模組解析 ,這篇文件寫的很詳細,足以解釋。這裡列舉兩個我認為的重點:相對模組匯入不能解析為一個外部模組宣告

。自己宣告的模組(declare module “name”{} 這個name不能是相對的也就是說不能以 ./``````../``````/開頭),還有一個是配置檔案tsconfig.json檔案中{"compilerOptions": {"traceResolution": true}}可以追蹤模組解析的過程,想要深入瞭解這是一個好的方法,但是據我觀察只能在vscode控制檯中才能打印出來。

注:TS引用JS庫如果JS庫沒有對應的宣告檔案編譯器是不會報錯的,因為沒有宣告檔案的JS模組會隱式的獲得any型別,除非tsconfig.json中有noImplicitAny: true這樣的配置。

已經過驗證上面的解釋是沒錯的還原如下:

注:TS引用JS庫如果JS庫沒有對應的宣告檔案編譯器是不會報錯的,因為沒有宣告檔案的JS模組會隱式的獲得any型別,除非tsconfig.json中有noImplicitAny: true這樣的配置,指明瞭如有隱式轉換為any型別就報錯,才會報錯。

TypeScript是JavaScript的超集

TypeScript會進型別檢查,什麼鬼?JS沒有這個東西

使用TS進行開發也可以使用當前豐富的JS庫,很多JS庫有寫好的TS宣告檔案,但是如果是我們自己寫的JS庫想要在TS中使用就需要我們自己去編寫宣告檔案(.d.ts檔案),怎麼寫?這就是極具個人經驗主義的本文要解釋的問題,如有謬誤感謝指正。

本文主要是對此刻所得的整理。

下面示例基於webpack配合ts-loader,開發環境的配置可以參考我的另一篇文章基於webpack3.x從0開始搭建React開發環境

JS庫檔案和對應的TS宣告檔案

現有的JS庫可能有不同的寫法,有的庫匯出屬性,方法等,有的庫匯出一個類還有的庫只匯出一個函式。下面針對不同型別的JS庫來寫不同的宣告檔案。

宣告檔案也分為兩種,一種是全域性型別宣告另一種是模組匯出宣告而這兩種只是宣告檔案的寫法和JS庫的寫法沒有關係,並不是說全域性的庫就需要使用全域性型別宣告的寫法,模組的庫就用模組匯出的寫法。

// 檔案目錄結構如下
-- project
 |-- node_modules
   |-- simple
     |-- index.js
     |-- lib1.js
     |-- lib2.js
 |-- src
   |-- types.d.ts
   |-- app.ts
// /node_modules/simple/index.js
// ES原生模組寫法,並且匯出了屬性和方法
let a = 1;

function geta () {
  return a;
}

function seta (val) {
  a = val
}

export {geta, seta, a, a as default}
// 為simple/index.js寫全域性型別宣告,在types.d.ts中新增如下程式碼
declare module "simple" {
  let a: number;
  export function geta(): void;
  export function seta(n: number): void;
  export default a;
}
// app.ts  使用三斜線指令引入宣告檔案
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'

console.log(a)
// /node_modules/simple/lib1.js
// 匯出一個類
function Ab () {
  this.a = 1
}

Ab.prototype.seta = function (num) {
  this.a = num
}

Ab.prototype.geta = function (num) {
  return this.a
}

exports.Ab = Ab
// 在type.d.ts檔案中新增

declare module "simple/lib1" {
  export class Ab {
    private a;
    seta(n: number): void;
    geta(): number
  }
}
// app.ts  使用三斜線指令引入宣告檔案
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'

// 得以驗證
console.log(new Ab())

console.log(a)
// /node_modules/simple/lib2.js
// 只匯出一個函式
module.exports = function getRandom () {
  return Math.random()
}
// 在type.d.ts檔案中新增
declare module "simple/lib2" {
  let getRandom: () => number;
  export = getRandom;
}
// app.ts  使用三斜線指令引入宣告檔案
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'

import {Ab} from 'simple/lib1'

import getRandom = require('simple/lib2')

// 得以驗證
console.log(getRandom())

console.log(new Ab())

console.log(a)

上面給出的只是全域性宣告的寫法,下面會針對上面的js庫重新換成模組匯出宣告的寫法

目錄改成如下形式,app.ts檔案無需做大的改動只需要將三斜線指令去除即可,一般情況下即使去除該指令types.d.ts檔案還在的話TypeScript編譯器還是會將該檔案載入編譯,這與配置有關。

並且根據我的觀察發現,修改宣告檔案並不會馬上起作用,比如在宣告檔案中加了一個方法,在使用的時候TypeScript編譯器還是會報錯說這個型別沒有這個方法,需要重啟webpack-dev-server(我用的是這個)

// 檔案目錄結構如下
-- project
 |-- node_modules
   |-- simple
     |-- index.js
     |-- index.d.ts
     |-- lib1.js
     |-- lib1.d.ts
     |-- lib2.js
     |-- lib2.d.ts
 |-- src
   |-- app.ts
// index.d.ts
let a: number;
export function geta(): void;
export function seta(n: number): void;
export default a;
// app.ts
import a, {geta, seta} from 'simple'

// 得以驗證
console.log(geta())
//lib1.d.ts
function Ab () {
  this.a = 1
}

Ab.prototype.seta = function (num) {
  this.a = num
}

Ab.prototype.geta = function () {
  return this.a
}
exports.Ab = Ab
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'

// 得以驗證
console.log(new Ab().geta())

console.log(geta())
// lib2.d.ts
let getRandom: () => number;
export = getRandom;
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'
import getRandom = require('simple/lib2')

// 得以驗證
console.log(getRandom())

console.log(new Ab().geta())

console.log(geta())

不想為JS類庫寫具體的宣告檔案怎麼辦?

在全域性型別宣告的檔案中宣告一個模組,模組什麼都不做即可(這裡還可以更加徹底,如文章開頭的更新):

// types.d.ts  替換simple宣告如下
decalre module "simple"

node_modules下的@types資料夾

預設所有可見的"@types"包會在編譯過程中被包含進來。什麼叫預設可見?就是說node_modules/@types資料夾及他的子資料夾下所有的包都是可見的,還包括 ../node_modules/@types../../node_modules/@types等。

有什麼用呢?可以將自己的全域性宣告檔案放在這個資料夾裡面,這樣就可以自動載入。

上面一段話也是錯的,並不會自動的匯入,所上面摘抄官網的一段話到底啥意思就不得而知了。可能意思是會被包含但是就看能不能正確解析了。

注:node_modules/@types 是TS宣告檔案預設位置,並且只能是全域性宣告的寫法

上面一句話就錯了,@types裡面的宣告可以是模組的寫法,也可以是全域性寫法。只要是一個模組(頂層的import/export這個檔案就是一個模組或者declare “name” module { export … })就行了。

全部重寫node_modules下的@types資料夾如下

在@types資料夾下的宣告的包和在node_modules下的包其實一樣,在node_modules沒有解析到合適的.ts/.tsx/.d.ts 檔案之後,TS編譯器便會來到node_modules/@types檔案加下尋找,如下:

import {a} from 'abc'

這種情況@/types/abc.d.ts有兩種寫法

export const a: number; // 只要匯出即可
declare module "abc"{
  export const a: number;
}

同樣這個@/types/abc.d.ts的目錄也可以這樣:

@types/abc/index.d.ts

關於@types的配置

這個配置有兩個:一個是typeRoots表明宣告檔案的根資料夾,還有一個是types表明需要包含的宣告檔案包(資料夾名,我試過types/efg.d.ts 這樣的並不能用,需要這樣types/efg/index.d.ts)。

上面劃掉的部分是錯的,可以用types/efg.d.ts這樣的。

注驗證的時候最好配合types因為上文提到過,TS編譯器會預設包含所有的ts檔案,所以如果不過濾,設定的typeRoots沒有意義因為預設就是全部包含的。

極具個人經驗部分

觀察上面模組匯出宣告和全域性型別宣告兩種寫法發現寫法差別並不大,主要區別就是宣告檔案放置位置不同,全域性會多一個declare module "name1"。再仔細觀察會發現這個name1和import … from "name2"中的name2是一樣的,然後對於全域性的宣告檔案還在需要的時候使用 /// <reference path = "path" />引用進來。所以我懷疑(因為我還沒有了解到是不是事實)import … from "name"這個其實引用的是我們在宣告檔案中定義的module。

什麼是module?

如果一個JS檔案在頂層具有import或者export那麼這個檔案就是一個模組(模組名對應的就是檔名),在模組中定義的變數並不會暴露在全域性環境下。

而上面模組匯出的寫法 declare module "name"{}就相當於聲明瞭一個模組(一個檔案?)。

全域性型別宣告的思考

全域性型別宣告中只是聲明瞭相關模組當然也可以宣告其他東西,而是用全域性型別宣告的方法,不是import(這不是一個模組)而是三斜線指令使用 /// <reference path="path" />這樣 .d.ts 檔案中的宣告就被編譯器讀取了,之後可以再下面import … from "module"只是這個module是我們宣告出來的,並不會在對應的路徑下找到相關的 .d.ts.ts.tsx檔案。

如果是模組匯出寫法必須和庫在一起,否則並不知道屬於哪個模組的宣告,但是@types怎麼解釋

模組匯出宣告的思考

模組匯出宣告的寫法是在 .d.ts 檔案頂層是有export的所以一個檔案是一個模組,如果單獨引入(要使用import來引入)模組的話並不知道這個模組是哪個庫的宣告檔案,所以需要和JS庫放在一起,並且名字還要一樣(字尾名不一樣)。但是@types怎麼解釋???

關於@types的思考

見上文node_modules下的@types資料夾

參考