유형 스크립트를 사용한 인터페이스 유형 검사
유형 스크립트를 사용한 인터페이스 유형 검사
이 질문은 에 대한 직접적인 유사점이다
나는 런타임에 어떤 유형의 변수가 인터페이스를 구현하는지 알아봐야 한다. 내 코드는 다음과 같습니다:
interface A{
member:string;
}
var a:any={member:"foobar"};
if(a instanceof A) alert(a.member);
이 코드를 타이프스크립트 플레이그라운드에 입력하면 마지막 줄이 "A라는 이름이 현재 스코프에 존재하지 않습니다"라는 오류로 표시됩니다. 하지만 그것은 사실이 아니다, 그 이름은 현재 범위에 존재한다. 나는 심지어 편집자의 불평 없이 변수 선언을 변경할 수 있다. 웹을 검색하고 SO에서 다른 질문을 찾은 후 인터페이스를 클래스로 변경했지만 객체 리터럴을 사용하여 인스턴스를 만들 수 없습니다.
나는 어떻게 A타입이 그렇게 사라질 수 있는지 궁금했지만 생성된 자바스크립트를 보면 문제가 설명된다:
var a = {
member: "foobar"
};
if(a instanceof A) {
alert(a.member);
}
A를 인터페이스로 표현하지 않아 런타임 유형 검사가 불가능하다.
동적 언어로서의 자바스크립트에는 인터페이스 개념이 없는 것으로 알고 있다. 인터페이스에 대한 type check 방법이 있나요?
타자기 놀이터의 자동 완성은 타자기가 심지어 방법을 제공한다는 것을 보여준다. 어떻게 사용하나요?
이제 사용자 지정 유형 가드를 작성할 수 있으므로 키워드 없이 원하는 작업을 수행할 수 있습니다:
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);
}
나는 TypeScript가 객체가 특정 인터페이스를 구현하는지 동적으로 테스트하기 위한 직접적인 메커니즘을 제공하지 않는다는 것을 지적하고 싶다.
대신에 타입스크립트 코드는 자바스크립트 기법을 사용하여 오브젝트에 적절한 멤버 집합이 있는지 확인할 수 있다. 예:
var obj : any = new Foo();
if (obj.someInterfaceMethod) {
...
}
이제 가능합니다, 저는 완전한 반사 기능을 제공하는 향상된 버전의 컴파일러를 방금 출시했습니다. 메타데이터 개체에서 클래스를 인스턴스화하고, 클래스 생성자에서 메타데이터를 검색하고, 런타임에 인터페이스/클래스를 검사할 수 있습니다. 확인할 수 있습니다
사용 예:
다음과 같이 유형 스크립트 파일 중 하나에서 인터페이스와 이를 구현하는 클래스를 만듭니다:
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)
}
리플렉트로 컴파일하여 실행:
$ node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function
메타 유형에 대한 자세한 내용은 reflection.d.ts를 참조하십시오.
전체 작업 예제를 찾을 수 있습니다
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;
}
그리고 그것은 또한 작동한다.
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;
}
}
사용자 정의 유형 가드는 어떻습니까?
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();
}
유형 가드
interface MyInterfaced {
x: number
}
function isMyInterfaced(arg: any): arg is MyInterfaced {
return arg.x !== undefined;
}
if (isMyInterfaced(obj)) {
(obj as MyInterfaced ).x;
}
여기 다른 옵션이 있습니다. 모듈은 TypeScript 인터페이스를 런타임 설명자로 변환하는 빌드 타임 도구를 제공하며 객체가 이를 충족하는지 확인할 수 있습니다.
OP의 예를 들면,
interface A {
member: string;
}
먼저 다음과 같이 사용할 수 있는 설명자(예: )가 포함된 새로운 간결한 파일을 생성하는 것을 실행합니다:
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); }
위와 같으나 이번에는 화살표 함수 술어가 사용되었다
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);
런타임에 유형을 알 수 없기 때문에 알 수 없는 개체를 유형이 아닌 알려진 개체와 비교하기 위해 다음과 같이 코드를 작성했습니다:
- 올바른 유형의 샘플 개체 만들기
- 선택적 요소 지정
- 이 샘플 개체에 대해 알 수 없는 개체를 자세히 비교합니다
상세 비교에 사용하는 (인터페이스에 구애받지 않는) 코드는 다음과 같습니다:
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이 튜플의 배열을 포함할 것으로 예상하는데, 그 중 두 번째 요소는 (두 개의 선택적 요소를 갖는) 인터페이스의 인스턴스이다.
TypeScript의 Type-checking은 내 샘플 객체가 올바른지 확인한 다음 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);
}
사용자 정의 유형 가드 구현에서 이와 같은 검사를 호출할 수 있습니다.
Fenton's를 기반으로, 주어진 키가 완전히 또는 부분적으로 가지고 있는지 확인하기 위한 함수의 구현입니다.
사용 사례에 따라 인터페이스의 각 속성 유형을 확인해야 할 수도 있습니다. 아래 코드는 그렇게 하지 않습니다.
function implementsTKeys<T>(obj: any, keys: (keyof T)[]): obj is T {
if (!obj || !Array.isArray(keys)) {
return false;
}
const implementKeys = keys.reduce((impl, key) => impl && key in obj, true);
return implementKeys;
}
사용 예:
interface A {
propOfA: string;
methodOfA: Function;
}
let objectA: any = { propOfA: '' };
// Check if objectA partially implements A
let implementsA = implementsTKeys<A>(objectA, ['propOfA']);
console.log(implementsA); // true
objectA.methodOfA = () => true;
// Check if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);
console.log(implementsA); // true
objectA = {};
// Check again if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);
console.log(implementsA); // false, as objectA now is an empty object
export interface ConfSteps {
group: string;
key: string;
steps: string[];
}
private verify(): void {
const obj = `{
"group": "group",
"key": "key",
"steps": [],
"stepsPlus": []
} `;
if (this.implementsObject<ConfSteps>(obj, ['group', 'key', 'steps'])) {
console.log(`Implements ConfSteps: ${obj}`);
}
}
private objProperties: Array<string> = [];
private implementsObject<T>(obj: any, keys: (keyof T)[]): boolean {
JSON.parse(JSON.stringify(obj), (key, value) => {
this.objProperties.push(key);
});
for (const key of keys) {
if (!this.objProperties.includes(key.toString())) {
return false;
}
}
this.objProperties = null;
return true;
}
실행 시 다음과 같이 TypeScript 유형을 검증할 수 있습니다(Babel 플러그인이 필요함):
const user = validateType<{ name: string }>(data);
메서드나 인터페이스 이름을 재팩터링하려면 IDE가 이러한 문자열 리터럴을 재팩터링하지 않을 수 있기 때문에 문자열 리터럴로 작업하는 것은 어렵습니다. 인터페이스에 하나 이상의 방법이 있을 경우 작동하는 내 솔루션을 제공합니다
export class SomeObject implements interfaceA {
public methodFromA() {}
}
export interface interfaceA {
methodFromA();
}
개체가 인터페이스 유형인지 확인하십시오:
const obj = new SomeObject();
const objAsAny = obj as any;
const objAsInterfaceA = objAsAny as interfaceA;
const isObjOfTypeInterfaceA = objAsInterfaceA.methodFromA != null;
console.log(isObjOfTypeInterfaceA)
참고: 메서드가 SomeObject 클래스에 여전히 존재하기 때문에 'implements interface A'를 제거해도 됩니다
제가 수업을 이용해서 생각해낸 해결책은 다음과 같습니다 : (효과가 있습니다!)
// 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)) { ... }
나는 파일에서 예를 찾았다
체커
declare const isCompositeFilterDescriptor: (source: FilterDescriptor | CompositeFilterDescriptor) => source is CompositeFilterDescriptor;
사용 예
const filters: Array<FilterDescriptor | CompositeFilterDescriptor> = filter.filters;
filters.forEach((element: FilterDescriptor | CompositeFilterDescriptor) => {
if (isCompositeFilterDescriptor(element)) {
// element type is CompositeFilterDescriptor
} else {
// element type is FilterDescriptor
}
});
유형 스크립트의 유형 가드:
TS는 이러한 목적을 가지고 있습니다. 그들은 다음과 같은 방식으로 그것을 정의한다:
특정 범위에서 형식을 보장하는 런타임 검사를 수행하는 일부 식입니다.
이것은 기본적으로 TS 컴파일러가 충분한 정보를 가지고 있을 때 유형을 더 구체적인 유형으로 좁힐 수 있다는 것을 의미한다. 예:
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
}
}
다시 질문으로 돌아가서, 우리는 유형 가드의 개념을 객체에 적용하여 객체의 유형을 결정할 수도 있습니다. 객체에 대한 형식 가드를 정의하려면 반환 형식이 a인 함수를 정의해야 합니다. 예:
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();
}
내 생각에는 이것이 최선의 접근법이다; 인터페이스에 "Fubber" 기호를 붙인다. 작성 속도가 훨씬 빠르고, 자바스크립트 엔진의 경우 타입 가드보다 훨씬 빠르며, 인터페이스에 대한 상속을 지원하고, 필요할 때 타입 가드를 쉽게 쓸 수 있게 한다.
이것이 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 컴파일러를 돕고 싶다면 이 코드를 TypeGuard에 넣을 수 있다.
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
}
이 대답은 매우 간단하다. 그러나 이 해결책은 적어도 (항상 이상적이지는 않지만) 경우의 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
}
Reflect를 사용하여 유형 스크립트에 가드
다음은 내 Typescript 게임 엔진의 유형 가드의 예입니다
export interface Start {
/**
* Start is called on the frame when a script is enabled just before any of the Update methods are called the first time.
*/
start(): void
}
/**
* User Defined Type Guard for Start
*/
export const implementsStart = (arg: any): arg is Start => {
return Reflect.has(arg, 'start')
}
/**
* Example usage of the type guard
*/
start() {
this.components.forEach(component => {
if (implementsStart(component)) {
component.start()
}
})
}
OP한지 9년이 다 되어가는데, 이 문제는 여전히 남아있다. 나는 Typescript를 정말로 사랑하고 싶다. 그리고 보통 나는 성공한다. 하지만 타입 안전의 허점은 제 코가 꽉 막힌 악취입니다.
해결 방법이 완벽하지 않습니다. 하지만 내 의견은 그것들이 더 일반적으로 처방되는 대부분의 해결책들보다 더 낫다는 것이다. 판별기는 확장성을 제한하고 형식 안전의 목적을 모두 무시하기 때문에 나쁜 관행임이 입증되었다. 나의 가장 예쁜 두 가지 추악한 해결책은 다음과 같다:
클래스 장식자: 입력한 개체의 멤버를 재귀적으로 검색하고 기호 이름을 기준으로 해시를 계산합니다. 정적 KVP 속성의 형식 이름과 해시를 연결합니다. 해시 계산에 형식 이름을 포함하여 상위 클래스와의 모호성 위험을 줄입니다(빈 하위 클래스에서 발생). 찬성: 그것은 가장 신뢰할 수 있는 것으로 입증되었다. 그것은 또한 매우 엄격한 시행을 제공한다. 이것은 또한 다른 고급 언어들이 자연적으로 다형성을 구현하는 방법과 유사하다. 그러나 진정한 다형성을 위해서는 솔루션이 훨씬 더 확장되어야 한다. 단점: 익명/JSON 객체는 연결하고 정적으로 캐시할 유형 정의가 없기 때문에 모든 유형 검사에서 다시 캐시해야 합니다. 과도한 스택 오버헤드는 고부하 시나리오에서 상당한 성능 병목 현상을 초래합니다. IoC 컨테이너로 완화할 수 있지만, 다른 근거가 없는 소규모 앱의 경우 바람직하지 않은 오버헤드가 될 수도 있습니다. 또한 장식기를 필요한 모든 물체에 적용하기 위해서는 추가적인 노력이 필요합니다.
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.
I knew I'd stumbled across a github package that addressed this properly, and after trawling through my search history I finally found it. Check out typescript-is - though it requires your code to be compiled using ttypescript (I am currently in the process of bullying it into working with create-react-app, will update on the success/failure later), you can do all sorts of crazy things with it. The package is also actively maintained, unlike ts-validate-type.
You can check if something is a string or number and use it as such, without the compiler complaining:
import { is } from 'typescript-is';
const wildString: any = 'a string, but nobody knows at compile time, because it is cast to `any`';
if (is<string>(wildString)) { // returns true
// wildString can be used as string!
} else {
// never gets to this branch
}
if (is<number>(wildString)) { // returns false
// never gets to this branch
} else {
// Now you know that wildString is not a number!
}
You can also check your own interfaces:
import { is } from 'typescript-is';
interface MyInterface {
someObject: string;
without: string;
}
const foreignObject: any = { someObject: 'obtained from the wild', without: 'type safety' };
if (is<MyInterface>(foreignObject)) { // returns true
const someObject = foreignObject.someObject; // type: string
const without = foreignObject.without; // type: string
}
Simple workaround solution having the same drawbacks as the selected solution, but this variant catches JS errors, only accepts objects as parameter, and has a meaningful return value.
interface A{
member:string;
}
const implementsA = (o: object): boolean => {
try {
return 'member' in o;
} catch (error) {
return false;
}
}
const a:any={member:"foobar"};
implementsA(a) && console.log("a implements A");
// implementsA("str"); // causes TS transpiler error
Another solution could be something similar what is used in case of HTMLIFrameElement interface. We can declare a variable with the same name by creating an object by the interface if we know that there is an implementation for it in another module.
declare var HTMLIFrameElement: {
prototype: HTMLIFrameElement;
new(): HTMLIFrameElement;
};
So in this situation
interface A {
member:string;
}
declare var A : {
prototype: A;
new(): A;
};
if(a instanceof A) alert(a.member);
should work fine
You can also send multiple inputs to child components, having one be a discriminator, and the other being the actual data, and checking the discriminator in the child component like this:
@Input() data?: any;
@Input() discriminator?: string;
ngOnInit(){
if(this.discriminator = 'InterfaceAName'){
//do stuff
}
else if(this.discriminator = 'InterfaceBName'){
//do stuff
}
}
Obviously you can move this into wherever it is applicable to use, like an ngOnChanges function or a setter function, but the idea still stands. I would also recommend trying to tie an ngModel to the input data if you want a reactive form. You can use these if statements to set the ngModel based on the data being passed in, and reflect that in the html with either:
<div [(ngModel)]={{dataModel}}>
<div *ngFor="let attr of (data | keyvalue)">
<!--You can use attr.key and attr.value in this situation to display the attributes of your interface, and their associated values from the data -->
</div>
</div>
Or This Instead:
<div *ngIf = "model == 'InterfaceAName'">
<div>Do This Stuff</div>
</div>
<div *ngIf= "model == 'IntefaceBName'">
<div>Do this instead</div>
</div>
(You can use attr.key and attr.value in this situation to display the attributes of your interface, and their associated values from the data)
I know the question is already answered, but I thought this might be useful for people trying to build semi-ambiguous angular forms. You can also use this for angular material modules (dialog boxes for example), by sending in two variables through the data parameter--one being your actual data, and the other being a discriminator, and checking it through a similar process. Ultimately, this would allow you to create one form, and shape the form around the data being flowed into it.
I know the question is a bit old, but just my 50 cents. This worked for me:
const container: Container = icc.controlComponent as unknown as Container;
if (container.getControlComponents) {
this.allControlComponents.push(...container.getControlComponents());
}
Container
is the interface, and icc.controlComponent
is the object I wanted to check, and getControlComponents
is a method from Container
interface.
Not tested...
interface MySuperInterface {}
interface myInterface extends MySuperInterface {
myObjectVariable: any
}
if ((myObject as MyInterface).myObjectVariable !== undefined)
The premise of the question is that interface should provide runtime identification of what objects implement a given interface. But that's not how Interface has been designed in TypeScript because, as both the OP and others have said, Interfaces simply do not exist at runtime. The implications of that design choice is that at runtime, interfaces are no more than a collection of JavaScript object properties.
To do a runtime check, you can do the hacky thing of checking for the individual members in the interface:
interface A{
member:string;
}
if(a.member) alert(a.member);
This is essentially what a lot of the proposed workarounds, including type guards, boil down to. This has obvious limitation when there are multiple members in the interface (should I check for them all? should I define an interface identifier/discriminator property?) and will in some cases require additional type checks on the members to pass the TS compiler's type checker. (E.g., another interface might implement member
as number).
If the idea is to allow objects to exhibit behavior depending on what interfaces are present, then a better pattern is to use a base interface with a common method (or methods) to trigger behavior and then extend the base interface to add specialized properties and/or methods. Extending the OPs example:
interface A { //Base interface that all objects in the collection will implement
talk():string
}
interface B extends A { //Specialized interface
member: string
}
var a:A = {
talk() {return 'got no member'}
}
var b:B = {
member: 's',
talk() {return 'my member is '+this.member}
}
for(let item of [a,b]) { //We can now loop over both object types and get the relevant response
console.log(item, item.talk());
}
The point being that implementations of either interface are forced to implement a talk method with a common spec, which is enforced by the type checker. The compiler's type checking becomes increasingly valuable as the interfaces grow in complexity.
The biggest weakness of TypeScript's Interface is the lack of default implementations. You can fake it by defining a default object implementation of the interface and copying from it, like var defaultA = {talk() {'no member'}}; var a = {...defaultA}
, but it would be much cleaner if it was possible to specify the defaults in the interface definition.
With many objects implementing the same interfaces, it is better to implement them as classes using the implements keyword in a class definition, which typically improves memory usage and performance. It also allows for multiple inheritance of interfaces and is another way to populate default behavior for objects.