这个问题直接类似于TypeScript中的类类型检查
我需要在运行时找出任何类型的变量是否实现了接口。这是我的代码:
interface A{
member:string;
}
var a:any={member:"foobar"};
if(a instanceof A) alert(a.member);
如果您在typescript游乐场中输入这段代码,最后一行将被标记为错误,“名称A不存在于当前作用域”。但事实并非如此,该名称确实存在于当前作用域中。我甚至可以更改变量声明为var a: a ={成员:"foobar"};没有编辑的抱怨。在浏览网页并找到其他问题后,我将接口更改为类,但我不能使用对象字面量来创建实例。
我想知道A类型是如何消失的,但看看生成的javascript就能解释这个问题:
var a = {
member: "foobar"
};
if(a instanceof A) {
alert(a.member);
}
没有将A表示为接口,因此不可能进行运行时类型检查。
我知道javascript作为一种动态语言没有接口的概念。是否有方法对接口进行类型检查?
typescript游乐场的自动完成显示typescript甚至提供了一个方法实现。我怎么使用它?
下面是我使用类和lodash想出的解决方案:(它有效!)
// TypeChecks.ts
import _ from 'lodash';
export class BakedChecker {
private map: Map<string, string>;
public constructor(keys: string[], types: string[]) {
this.map = new Map<string, string>(keys.map((k, i) => {
return [k, types[i]];
}));
if (this.map.has('__optional'))
this.map.delete('__optional');
}
getBakedKeys() : string[] {
return Array.from(this.map.keys());
}
getBakedType(key: string) : string {
return this.map.has(key) ? this.map.get(key) : "notfound";
}
}
export interface ICheckerTemplate {
__optional?: any;
[propName: string]: any;
}
export function bakeChecker(template : ICheckerTemplate) : BakedChecker {
let keys = _.keysIn(template);
if ('__optional' in template) {
keys = keys.concat(_.keysIn(template.__optional).map(k => '?' + k));
}
return new BakedChecker(keys, keys.map(k => {
const path = k.startsWith('?') ? '__optional.' + k.substr(1) : k;
const val = _.get(template, path);
if (typeof val === 'object') return val;
return typeof val;
}));
}
export default function checkType<T>(obj: any, template: BakedChecker) : obj is T {
const o_keys = _.keysIn(obj);
const t_keys = _.difference(template.getBakedKeys(), ['__optional']);
return t_keys.every(tk => {
if (tk.startsWith('?')) {
const ak = tk.substr(1);
if (o_keys.includes(ak)) {
const tt = template.getBakedType(tk);
if (typeof tt === 'string')
return typeof _.get(obj, ak) === tt;
else {
return checkType<any>(_.get(obj, ak), tt);
}
}
return true;
}
else {
if (o_keys.includes(tk)) {
const tt = template.getBakedType(tk);
if (typeof tt === 'string')
return typeof _.get(obj, tk) === tt;
else {
return checkType<any>(_.get(obj, tk), tt);
}
}
return false;
}
});
}
自定义类:
// MyClasses.ts
import checkType, { bakeChecker } from './TypeChecks';
class Foo {
a?: string;
b: boolean;
c: number;
public static _checker = bakeChecker({
__optional: {
a: ""
},
b: false,
c: 0
});
}
class Bar {
my_string?: string;
another_string: string;
foo?: Foo;
public static _checker = bakeChecker({
__optional: {
my_string: "",
foo: Foo._checker
},
another_string: ""
});
}
在运行时检查类型:
if (checkType<Bar>(foreign_object, Bar._checker)) { ... }
自OP以来将近9年,这个问题仍然存在。我真的很想爱上Typescript。通常我都能成功。但它在打字安全方面的漏洞是我捏着的鼻子挡不住的恶臭。
我的解决方案并不完美。但我的观点是,它们比大多数更常用的解决方案要好。鉴别符已被证明是一种糟糕的实践,因为它们限制了可伸缩性并完全违背了类型安全的目的。我的两个最漂亮的解决方案是,按顺序排列:
Class Decorator:
Recursively scans the typed object's members and computes a hash based on the symbol names. Associates the hash with the type name in a static KVP property. Include the type name in the hash calculation to mitigate risk of ambiguity with ancestors (happens with empty subclasses).
Pros: It's proven to be the most trustworthy. It is also provides very strict enforcements. This is also similar to how other high-level languages natively implement polymorphism. Howbeit, the solution requires much further extension in order to be truly polymorphic.
Cons: Anonymous/JSON objects have to be rehashed with every type check, since they have no type definitions to associate and statically cache. Excessive stack overhead results in significant performance bottlenecks in high load scenarios. Can be mitigated with IoC containers, but that can also be undesirable overhead for small apps with no other rationale. Also requires extra diligence to apply the decorator to every object requiring it.
Cloning:
Very ugly, but can be beneficial with thoughtful strategies. Create a new instance of the typed object and reflexively copy the top-level member assignments from the anonymous object. Given a predetermined standard for passage, you can simultaneously check and clone-cast to types. Something akin to "tryParse" from other languages.
Pros: In certain scenarios, resource overhead can be mitigated by immediately using the converted "test" instance. No additional diligence required for decorators. Large amount of flexibility tolerances.
Cons: Memory leaks like a flour sifter. Without a "deep" clone, mutated references can break other components not anticipating the breach of encapsulation. Static caching not applicable, so operations are executed on each and every call--objects with high quantities of top-level members will impact performance. Developers who are new to Typescript will mistake you for a junior due to not understanding why you've written this kind of pattern.
All totalled: I don't buy the "JS doesn't support it" excuse for Typescript's nuances in polymorphism. Transpilers are absolutely appropriate for that purpose. To treat the wounds with salt: it comes from Microsoft. They've solved this same problem many years ago with great success: .Net Framework offered a robust Interop API for adopting backwards compatibility with COM and ActiveX. They didn't try to transpile to the older runtimes. That solution would have been much easier and less messy for a loose and interpreted language like JS...yet they cowered out with the fear of losing ground to other supersets. Using the very shortcomings in JS that was meant to be solved by TS, as a malformed basis for redefining static typed Object-Oriented principle is--well--nonsense. It smacks against the volumes of industry-leading documentation and specifications which have informed high-level software development for decades.
下面是我使用类和lodash想出的解决方案:(它有效!)
// TypeChecks.ts
import _ from 'lodash';
export class BakedChecker {
private map: Map<string, string>;
public constructor(keys: string[], types: string[]) {
this.map = new Map<string, string>(keys.map((k, i) => {
return [k, types[i]];
}));
if (this.map.has('__optional'))
this.map.delete('__optional');
}
getBakedKeys() : string[] {
return Array.from(this.map.keys());
}
getBakedType(key: string) : string {
return this.map.has(key) ? this.map.get(key) : "notfound";
}
}
export interface ICheckerTemplate {
__optional?: any;
[propName: string]: any;
}
export function bakeChecker(template : ICheckerTemplate) : BakedChecker {
let keys = _.keysIn(template);
if ('__optional' in template) {
keys = keys.concat(_.keysIn(template.__optional).map(k => '?' + k));
}
return new BakedChecker(keys, keys.map(k => {
const path = k.startsWith('?') ? '__optional.' + k.substr(1) : k;
const val = _.get(template, path);
if (typeof val === 'object') return val;
return typeof val;
}));
}
export default function checkType<T>(obj: any, template: BakedChecker) : obj is T {
const o_keys = _.keysIn(obj);
const t_keys = _.difference(template.getBakedKeys(), ['__optional']);
return t_keys.every(tk => {
if (tk.startsWith('?')) {
const ak = tk.substr(1);
if (o_keys.includes(ak)) {
const tt = template.getBakedType(tk);
if (typeof tt === 'string')
return typeof _.get(obj, ak) === tt;
else {
return checkType<any>(_.get(obj, ak), tt);
}
}
return true;
}
else {
if (o_keys.includes(tk)) {
const tt = template.getBakedType(tk);
if (typeof tt === 'string')
return typeof _.get(obj, tk) === tt;
else {
return checkType<any>(_.get(obj, tk), tt);
}
}
return false;
}
});
}
自定义类:
// MyClasses.ts
import checkType, { bakeChecker } from './TypeChecks';
class Foo {
a?: string;
b: boolean;
c: number;
public static _checker = bakeChecker({
__optional: {
a: ""
},
b: false,
c: 0
});
}
class Bar {
my_string?: string;
another_string: string;
foo?: Foo;
public static _checker = bakeChecker({
__optional: {
my_string: "",
foo: Foo._checker
},
another_string: ""
});
}
在运行时检查类型:
if (checkType<Bar>(foreign_object, Bar._checker)) { ... }
因为在运行时类型是未知的,所以我写了如下代码来比较未知对象,不是与类型进行比较,而是与已知类型的对象进行比较:
创建正确类型的示例对象
指定它的哪些元素是可选的
将你的未知对象与这个样本对象进行深度比较
下面是我用于深度比较的(接口不可知)代码:
function assertTypeT<T>(loaded: any, wanted: T, optional?: Set<string>): T {
// this is called recursively to compare each element
function assertType(found: any, wanted: any, keyNames?: string): void {
if (typeof wanted !== typeof found) {
throw new Error(`assertType expected ${typeof wanted} but found ${typeof found}`);
}
switch (typeof wanted) {
case "boolean":
case "number":
case "string":
return; // primitive value type -- done checking
case "object":
break; // more to check
case "undefined":
case "symbol":
case "function":
default:
throw new Error(`assertType does not support ${typeof wanted}`);
}
if (Array.isArray(wanted)) {
if (!Array.isArray(found)) {
throw new Error(`assertType expected an array but found ${found}`);
}
if (wanted.length === 1) {
// assume we want a homogenous array with all elements the same type
for (const element of found) {
assertType(element, wanted[0]);
}
} else {
// assume we want a tuple
if (found.length !== wanted.length) {
throw new Error(
`assertType expected tuple length ${wanted.length} found ${found.length}`);
}
for (let i = 0; i < wanted.length; ++i) {
assertType(found[i], wanted[i]);
}
}
return;
}
for (const key in wanted) {
const expectedKey = keyNames ? keyNames + "." + key : key;
if (typeof found[key] === 'undefined') {
if (!optional || !optional.has(expectedKey)) {
throw new Error(`assertType expected key ${expectedKey}`);
}
} else {
assertType(found[key], wanted[key], expectedKey);
}
}
}
assertType(loaded, wanted);
return loaded as T;
}
下面是我如何使用它的一个例子。
在本例中,我期望JSON包含一个元组数组,其中第二个元素是一个名为User的接口实例(它有两个可选元素)。
TypeScript的类型检查将确保我的示例对象是正确的,然后assertTypeT函数检查未知(从JSON加载的)对象是否与示例对象匹配。
export function loadUsers(): Map<number, User> {
const found = require("./users.json");
const sample: [number, User] = [
49942,
{
"name": "ChrisW",
"email": "example@example.com",
"gravatarHash": "75bfdecf63c3495489123fe9c0b833e1",
"profile": {
"location": "Normandy",
"aboutMe": "I wrote this!\n\nFurther details are to be supplied ..."
},
"favourites": []
}
];
const optional: Set<string> = new Set<string>(["profile.aboutMe", "profile.location"]);
const loaded: [number, User][] = assertTypeT(found, [sample], optional);
return new Map<number, User>(loaded);
}
您可以在用户定义的类型保护的实现中调用这样的检查。
答案很简单。然而,这种解决方案至少在大约3/4的情况下是可能的(尽管并不总是理想的)。所以,换句话说,这可能与阅读这篇文章的人有关。
假设我有一个非常简单的函数,需要知道参数的接口类型:
const simpleFunction = (canBeTwoInterfaces: interfaceA | interface B) => {
// if interfaceA, then return canBeTwoInterfaces.A
// if interfaceB, then return canBeTwoInterfaces.B
}
得到最多赞的答案往往是使用“功能检查”。也就是说,
const simpleFunction = (canBeTwoInterfaces: interfaceA | interface B) => {
if (canBeTwoInterfaces.onlyExistsOnInterfaceA) return canBeTwoInterfaces.A
else return canBeTwoInterfaces.B
}
然而,在我正在使用的代码库中,我需要检查的接口主要包含可选参数。另外,我团队里的其他人可能会在我不知情的情况下突然改名字。如果这听起来像您正在使用的代码库,那么下面的函数要安全得多。
就像我之前说的,这对很多人来说可能是一件非常明显的事情。尽管如此,要知道何时何地应用给定的解决方案并不明显,不管它是否恰好像下面这样非常简单。
这就是我要做的:
const simpleFunction = (
canBeTwoInterfaces: interfaceA | interface B,
whichInterfaceIsIt: 'interfaceA' | 'interfaceB'
) => {
if (whichInterfaceIsIt === 'interfaceA') return canBeTwoInterface.A
else return canBeTwoInterfaces.B
}
现在这是可能的,我刚刚发布了一个增强版的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)
}
使用reflect -ts编译并启动它:
$ node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function
有关接口元类型的详细信息,请参阅reflect .d.ts。
更新:
您可以在这里找到一个完整的工作示例