如何在 TypeScript 中进行运行时类型转换?

Posted

技术标签:

【中文标题】如何在 TypeScript 中进行运行时类型转换?【英文标题】:How to do runtime type casting in TypeScript? 【发布时间】:2015-11-17 00:12:12 【问题描述】:

目前我正在做一个 typescript 项目,我真的很喜欢 TypeScript 带来的类型推断。但是 - 当从 HTTP 调用中获取对象时 - 我可以将它们转换为所需的类型,获取代码完成并在它们上调用函数编译时间,但这些会导致错误运行时

示例:

class Person
    name: string;

    public giveName() 
        return this.name;
    

    constructor(json: any) 
        this.name = json.name;
    


var somejson =  'name' : 'John' ; // Typically from AJAX call
var john = <Person>(somejson);      // The cast

console.log(john.name);       // 'John'
console.log(john.giveName()); // 'undefined is not a function'

虽然这编译得很好 - 并且智能感知建议我使用该函数,但它给出了运行时异常。解决方案可能是:

var somejson =  'name' : 'Ann' ;
var ann = new Person(somejson);

console.log(ann.name);        // 'Ann'
console.log(ann.giveName());  // 'Ann'

但这需要我为所有类型创建构造函数。特别是,在处理树状类型和/或来自 AJAX 调用的集合时,必须遍历所有项目并为每个项目新建一个实例。

所以我的问题:有没有更优雅的方法来做到这一点?也就是说,转换为一个类型并立即为其提供原型函数?

【问题讨论】:

类型转换纯粹是对编译器/IDE 的提示。它永远不会向您的普通 javascript 对象添加方法。因此,您需要将 JSON 数据实例化为 Person 对象,这是没有办法的。您可以轻松编写一个实用函数来为您实例化对象数组/树。在那张纸条上,投射将如何帮助您处理一组对象?您仍然需要遍历数组并转换每个元素,对吧? 【参考方案1】:

@DavidSherret 的 accepted answer 不适用于 TypeScript 3.5.1 及以后的版本。有关详细信息,请参阅问题#31661。

以下是在 TypeScript 上测试的更新版本4.3.2

function instanceFromJSON<T>(
  Type:  new(): T; ,
  jsonObj: Record<string, unknown>,
): T 
  const obj = new Type();

  for (const prop in jsonObj) 
    if (Object.prototype.hasOwnProperty.call(jsonObj, prop)) 
      const key = prop as keyof T;

      obj[key] = jsonObj[prop] as T[keyof T];
    
  

  return obj;

【讨论】:

【参考方案2】:

你可以使用Object.assign,例如:

var somejson =  'name' : 'Ann' ;
var ann = Object.assign(new Person, somejson);

console.log(ann.name);        // 'Ann'
console.log(ann.giveName());  // 'Ann'

但是如果你有嵌套类,你必须映射抛出对象并为每个元素分配。

【讨论】:

【参考方案3】:

类的原型可以动态影响到对象:

function cast<T>(obj: any, cl:  new(...args): T ): T 
  obj.__proto__ = cl.prototype;
  return obj;


var john = cast(/* somejson */, Person);

见the documentation of __proto__ here。

【讨论】:

J'aime lire la documentation en francais ;) 只要类没有任何箭头函数,此解决方案就可以工作。 哈哈!对不起!我编辑了。属性(包括箭头函数)存储在对象实例中,而不是原型中。是的,此解决方案不会在对象中添加任何属性。【参考方案4】:

查看编译后的 JavaScript,您会看到类型断言(强制转换)消失了,因为它仅用于编译。现在你告诉编译器somejson 对象的类型是Person。编译器相信你,但在这种情况下,这不是真的。

所以这个问题是一个运行时 JavaScript 问题。

让这个工作的主要目标是以某种方式告诉 JavaScript 类之间的关系是什么。所以...

    找到一种方法来描述类之间的关系。 创建一些东西以根据此关系数据自动将 json 映射到类。

有很多方法可以解决这个问题,但我会举个例子。这应该有助于描述需要做什么。

假设我们有这个类:

class Person 
    name: string;
    child: Person;

    public giveName() 
        return this.name;
    

还有这个json数据:

 
    name: 'John', 
    child: 
        name: 'Sarah',
        child: 
            name: 'Jacob'
        
    

要自动将其映射为 Person 的实例,我们需要告诉 JavaScript 类型是如何相关的。我们不能使用 TypeScript 类型信息,因为一旦它被编译,我们就会丢失它。一种方法是在描述它的类型上使用静态属性。例如:

class Person 
    static relationships = 
        child: Person
    ;

    name: string;
    child: Person;

    public giveName() 
        return this.name;
    

下面是一个可重用函数的示例,它根据此关系数据为我们创建对象:

function createInstanceFromJson<T>(objType:  new(): T; , json: any) 
    const newObj = new objType();
    const relationships = objType["relationships"] || ;

    for (const prop in json) 
        if (json.hasOwnProperty(prop)) 
            if (newObj[prop] == null) 
                if (relationships[prop] == null) 
                    newObj[prop] = json[prop];
                
                else 
                    newObj[prop] = createInstanceFromJson(relationships[prop], json[prop]);
                
            
            else 
                console.warn(`Property $prop not set because it already existed on the object.`);
            
        
    

    return newObj;

现在下面的代码可以工作了:

const someJson =  
        name: 'John', 
        child: 
            name: 'Sarah',
            child: 
                name: 'Jacob'
            
        
    ;
const person = createInstanceFromJson(Person, someJson);

console.log(person.giveName());             // John
console.log(person.child.giveName());       // Sarah
console.log(person.child.child.giveName()); // Jacob

Playground

理想情况下,最好的方法是使用实​​际读取 TypeScript 代码并创建一个保存类之间关系的对象。这样我们就不需要手动维护关系并担心代码更改。例如,现在使用这种设置重构代码有点风险。我不确定目前是否存在类似的东西,但绝对有可能。

替代解决方案

我刚刚意识到我已经用稍微不同的解决方案(虽然不涉及嵌套数据)回答了类似的问题。您可以在此处阅读以了解更多想法:

JSON to TypeScript class instance?

【讨论】:

似乎应该有一种方法可以跳过使用 objType 参数,因为您已经在函数定义中指定了泛型......也许不是,我正在阅读关于打字稿泛型现在看看是否有办法只需要传入json 参数... @DannyBullis 唯一的问题是因为它编译为 JavaScript 类型信息(包括泛型)在运行时消失。它可以通过分析打字稿代码本身然后从中生成一些代码来解决。这是一个更复杂的解决方案。我知道该怎么做,但我只需要抽出时间来写它(可能在假期我会这样做......我已经创建了一个提醒)。 啊,我才意识到这一点。我会继续前进。祝你好运,节日快乐。 @DannyBullis 谢谢,你也是。我稍后会在这里评论。 一个缺点是 Person 需要一个空的构造函数,并且由于 Typescript 允许单个构造函数,这是一个限制。

以上是关于如何在 TypeScript 中进行运行时类型转换?的主要内容,如果未能解决你的问题,请参考以下文章

如何不编译使用 TypeScript

不编译如何使用TypeScript

如何在 TypeScript 中使用装饰器

如何修复'Typescript“正在进行类型检查......”花费太长时间'?

如何在 TypeScript 中创建自定义类型

如何在 TypeScript 中 document.getElementById() 方法的类型转换期间处理 ESLint 错误?