網站搜尋

javascript - 使用 Typescript 進行介面類型檢查


這個問題是 TypeScript 中類型檢查的直接類比

我需要在運行時檢查一個 any 類型的變數是否實現了一個介面。這是我的程式碼:

interface A {
    member: string;
}
    
var a: any = { member: "foobar" };
    
if (a instanceof A) alert(a.member);

如果你在 TypeScript Playground 中輸入這段程式碼,最後一行會被標記為錯誤,「當前作用域中不存在名稱 A」。但這不是真的,名稱確實存在於當前作用域。我甚至可以將變數宣告改為 var a:A={member:"foobar"};,編輯器也不會有任何抱怨。在瀏覽網頁並找到 Stack Overflow 上的另一個問題後,我將介面改成了類別,但這樣就不能使用物件字面量來創建實例了。

我想知道 A 類型怎麼會這樣消失,但看看生成的 JavaScript 就能解釋這個問題:

var a = {
    member: "foobar"
};

if (a instanceof A) {
    alert(a.member);
}

A 沒有作為介面的表示,因此無法進行運行時類型檢查。

我理解 JavaScript 作為一種動態語言沒有介面的概念。是否有任何方法可以對介面進行類型檢查?

TypeScript Playground 的自動補全顯示 TypeScript 甚至提供了一個方法 implements。我該如何使用它?

答案#1

你可以在不使用 instanceof 關鍵字的情況下實現你想要的功能,因為你現在可以編寫自訂的類型保護函數:

interface A {
    member: string;
}

function instanceOfA(object: any): object is A {
    return 'member' in object;
}

var a: any = {member: "foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}

許多會員

如果你需要檢查大量的成員以確定某個對象是否符合你的類型,你可以改為添加一個辨識符。下面是最基本的例子,並且需要你自己管理辨識符……你需要更深入了解這些模式以確保避免重複的辨識符。

interface A {
    discriminator: 'I-AM-A';
    member: string;
}

function instanceOfA(object: any): object is A {
    return object.discriminator === 'I-AM-A';
}

var a: any = {discriminator: 'I-AM-A', member: "foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}

答案#2

在 TypeScript 1.6 中,用戶自定義的類型保護將完成這個工作。

interface Foo {
    fooProperty: string;
}

interface Bar {
    barProperty: string;
}

function isFoo(object: any): object is Foo {
    return 'fooProperty' in object;
}

let object: Foo | Bar;

if (isFoo(object)) {
    // `object` has type `Foo`.
    object.fooProperty;
} else {
    // `object` has type `Bar`.
    object.barProperty;
}

就像 Joe Yang 提到的:自從 TypeScript 2.0 以來,你甚至可以利用標記聯合類型的優勢。

interface Foo {
    type: 'foo';
    fooProperty: string;
}

interface Bar {
    type: 'bar';
    barProperty: number;
}

let object: Foo | Bar;

// You will see errors if `strictNullChecks` is enabled.
if (object.type === 'foo') {
    // object has type `Foo`.
    object.fooProperty;
} else {
    // object has type `Bar`.
    object.barProperty;
}

而且它也適用於 switch。

答案#3

使用者自訂型別保護如何?https://www.typescriptlang.org/docs/handbook/advanced-types.html

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function isFish(pet: Fish | Bird): pet is Fish { //magic happens here
    return (<Fish>pet).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

答案#4

TypeScript 2.0 引入了標記聯合

TypeScript 2.0 功能

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    // In the following switch statement, the type of s is narrowed in each case clause
    // according to the value of the discriminant property, thus allowing the other properties
    // of that variant to be accessed without a type assertion.
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}

答案#5

現在已經可以了,我剛發布了一個增強版本的 TypeScript 編譯器,它提供完整的反射功能。你可以從元數據對象實例化類,從類構造函數檢索元數據,並在運行時檢查介面/類。你可以在這裡查看它

使用範例:

在你的一個 TypeScript 檔案中,建立一個介面以及一個實現該介面的類別,如下所示:

interface MyInterface {
    doSomething(what: string): number;
}

class MyClass implements MyInterface {
    counter = 0;

    doSomething(what: string): number {
        console.log('Doing ' + what);
        return this.counter++;
    }
}

現在讓我們列印一些已實現介面的清單。

for (let classInterface of MyClass.getClass().implements) {
    console.log('Implemented interface: ' + classInterface.name)
}

使用 reflec-ts 編譯並啟動它:

node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function

請參閱 reflection.d.ts 以了解 Interface 元類型的詳細資訊。

更新:你可以在這裡找到完整的可運行範例

答案#6

這裡有另一個選擇:模組 ts-interface-builder 提供了一個構建時工具,可以將 TypeScript 介面轉換為運行時描述符,而 ts-interface-checker 可以檢查一個物件是否符合它。

對於 OP 的例子,

interface A {
  member: string;
}

你首先會執行 ts-interface-builder,它會生成一個帶有描述符的新簡潔文件,例如 foo-ti.ts,你可以這樣使用它:

import fooDesc from './foo-ti.ts';
import {createCheckers} from "ts-interface-checker";
const {A} = createCheckers(fooDesc);

A.check({member: "hello"});           // OK
A.check({member: 17});                // Fails with ".member is not a string" 

你可以創建一個單行的型別守衛函數:

function isA(value: any): value is A { return A.test(value); }

答案#7

TypeScript 中的類型保護:

TS 為此有 型別保護。它們用以下方式定義它:

某些表達式在運行時執行檢查,以保證某個範圍內的類型。

這基本上意味著當 TypeScript 編譯器有足夠的資訊時,它可以將類型縮小到更具體的類型。例如:

function foo (arg: number | string) {
    if (typeof arg === 'number') {
        // fine, type number has toFixed method
        arg.toFixed()
    } else {
        // Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
        arg.toFixed()
        // TSC can infer that the type is string because 
        // the possibility of type number is eliminated at the if statement
    }
}

回到你的問題,我們也可以將這個型別保護的概念應用到物件上,以判斷它們的型別。要為物件定義型別保護,我們需要定義一個回傳型別為型別斷言的函式。例如:

interface Dog {
    bark: () => void;
}

// The function isDog is a user defined type guard
// the return type: 'pet is Dog' is a type predicate, 
// it determines whether the object is a Dog
function isDog(pet: object): pet is Dog {
  return (pet as Dog).bark !== undefined;
}

const dog: any = {bark: () => {console.log('woof')}};

if (isDog(dog)) {
    // TS now knows that objects within this if statement are always type Dog
    // This is because the type guard isDog narrowed down the type to Dog
    dog.bark();
}

答案#8

距手術已接近9年,但這個問題仍然存在。我真的非常非常想喜歡 Typescript。通常我也能做得到。但它在類型安全上的漏洞是一股難聞的氣味,我屏住的鼻子也擋不住。

我常用的解決方案並不完美。但我認為它們比大多數較常被建議的解決方案要好。使用辨別器已被證明是不良的做法,因為它們限制了可擴展性,並完全違背了類型安全的目的。我兩個最漂亮又丑陋的解決方案依次是:

類別裝飾器:遞迴掃描已型別化物件的成員,並根據符號名稱計算哈希值。將該哈希值與類型名稱關聯於靜態 KVP 屬性中。在哈希計算中包含類型名稱,以降低與祖先發生歧義的風險(會發生在空子類別時)。優點:它已被證明是最可靠的,同時也提供非常嚴格的強制規範。這也類似於其他高階語言對多態性所做的本地實作。然而,該方案仍需進一步擴展,才能真正實現多態性。 缺點:匿名/JSON 對象在每次類型檢查時都必須重新哈希,因為它們沒有類型定義可以關聯並進行靜態緩存。在高負載場景下,過多的堆棧開銷會導致顯著的性能瓶頸。這可以通過 IoC 容器來緩解,但對於沒有其他理由的小型應用來說,這也可能是不希望的額外開銷。同時,還需要額外注意將裝飾器應用到每個需要它的對象上。

克隆:非常醜陋,但透過深思熟慮的策略可以帶來好處。創建一個類型化物件的新實例,並反射地複製匿名物件的頂層成員指派。給定一個既定的通過標準,你可以同時檢查並進行類型的克隆轉換。有點類似其他語言中的“tryParse”。優點:在某些情況下,可透過立即使用轉換後的“測試”實例來減少資源開銷。對裝飾器不需要額外的注意。 大量的靈活性容差。缺點:記憶體洩漏如同麵粉篩。沒有“深拷貝”的情況下,變異的引用可能會破壞其他沒有預料到封裝被突破的組件。靜態快取不適用,因此操作會在每次呼叫時執行——擁有大量頂層成員的物件會影響性能。對於剛接觸 Typescript 的開發者來說,可能會因為不理解你為什麼會寫出這種模式而把你誤認為是初級開發者。

總而言之:我不接受「JS 不支援」這個理由來解釋 TypeScript 在多態性上的細節。轉譯器對於這個目的來說完全是合適的。雪上加霜的是:這個問題來自微軟。他們多年前就已經成功解決了同樣的問題:.Net Framework 提供了強大的互操作 API,用於採用與 COM 和 ActiveX 的向後相容性。他們並沒有試圖轉譯到舊的執行環境。對於像 JS 這樣鬆散且解釋型的語言而言,這個解決方案會更簡單且不那麼混亂……然而他們因害怕在其他超集面前失去優勢而退縮。以 JS 本身的缺陷——本應由 TS 解決——作為重新定義靜態型別物件導向原則的畸形基礎——嗯——簡直是無稽之談。這與那些數十年來指導高階軟體開發的業界領導文件和規範背道而馳。

答案#9

在我看來,這是最好的方法;在介面上附加一個「Fubber」符號。它比使用類型守衛寫起來快得多,對 JavaScript 引擎來說也快得多,支持介面繼承,並且如果需要的話,讓類型守衛變得容易編寫。

這就是 ES6 有符號的目的。

介面

// Notice there is no naming conflict, because interfaces are a *type*
export const IAnimal = Symbol("IAnimal"); 
export interface IAnimal {
  [IAnimal]: boolean; // the fubber
}

export const IDog = Symbol("IDog");
export interface IDog extends IAnimal {
  [IDog]: boolean;
}

export const IHound = Symbol("IDog");
export interface IHound extends IDog {
  // The fubber can also be typed as only 'true'; meaning it can't be disabled.
  [IDog]: true;
  [IHound]: boolean;
}

課程

import { IDog, IAnimal } from './interfaces';
class Dog implements IDog {
  // Multiple fubbers to handle inheritance:
  [IAnimal] = true;
  [IDog] = true;
}

class Hound extends Dog implements IHound {
  [IHound] = true;
}

測試

如果你想幫助 TypeScript 編譯器,這段程式碼可以放在型別守衛中。

import { IDog, IAnimal } from './interfaces';

let dog = new Dog();

if (dog instanceof Hound || dog[IHound]) {
  // false
}
if (dog[IAnimal]?) {
  // true
}

let houndDog = new Hound();

if (houndDog[IDog]) {
  // true
}

if (dog[IDog]?) {
  // it definitely is a dog
}

答案#10

我想指出,TypeScript 並沒有提供直接的機制來動態測試一個物件是否實作了特定的介面。

相反,TypeScript 代碼可以使用 JavaScript 的技術來檢查物件上是否存在一組適當的成員。例如:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

答案#11

同上,使用了用戶自定義的守衛,但這次使用箭頭函數作為謂詞

interface A {
  member:string;
}

const check = (p: any): p is A => p.hasOwnProperty('member');

var foo: any = { member: "foobar" };
if (check(foo))
    alert(foo.member);