Home > Enterprise >  How to have an interface contain sub-types while also being a valid type by itself
How to have an interface contain sub-types while also being a valid type by itself

Time:10-18

I am trying to create a namespace/class/interface/type (whichever I need, I've researched all of them and none seem to be able to fulfill what I need), which has some properties and methods typing declared. But also have subtypes within it like Character.Stats or Character.Currency. While still having just Character as a valid type

How would I need to format/layout my typing file to allow this classes.ts file to be valid typescript?

// statement to import Character type

class Player implements Character {
    constructor (
        public name: string, // a property
        public stats: Character.Stats, // a property with a subtype typing
        public currency: Character.Currency
    ) {
        this.name = name
        this.stats = stats
        this.currency = currency
    }

    attack(target: Player) { // parameter is typing of its own class
        console.log(`Attacked ${Character.name}`)
    }
}

Is this even currently possible with typescript?

Below is my current test codebase:

types/items.ts

export interface Type {
    name: string,
    sprite: string,
}

types/characters.ts

import * as Classes from "../classes";

export interface Stats {
    health: number,
    attack: number,
    defence: number,
}

export interface Currency {
    gold: number,
    ridium: number
}

export interface Type {
    name: string,
    stats: Stats
    currency: Currency

    announce(sentence: string): void,
    attack(target: Type): void,
    heal(item: Classes.HealingItem): void

}

classes.ts

import * as Characters from "./types/characters";
import * as Items from "./types/items"

export class Character implements Characters.Type {

    constructor(
        public name: string,
        public stats: Characters.Stats,
        public currency: Characters.Currency
    ) {
        this.name = name;
        this.stats = stats
        this.currency = currency
    }

    announce(sentence: string) {
        console.log(`I, ${this.name}, ${sentence}`)
    }

    attack(target: Character): void {
        this.announce(`am going to attack ${target.name}`)
    }

    heal(item: HealingItem) {
        this.stats.health  = item.healthPoints;
        this.announce(`used ${item.name} to heal my hp from ${this.stats.health - item.healthPoints} to ${this.stats.health}`)
    }

}

export class HealingItem implements Items.Type {

    constructor(
        public name: string,
        public sprite: string,
        public healthPoints: number
    ) {
        this.name = name
        this.sprite = sprite
        this.healthPoints = healthPoints
    }

}

index.ts

import * as Classes from "./classes";

const hero = new Classes.Character("Bob", // name
    { // stats
        health: 100,
        attack: 10,
        defence: 25
    },
    { // currency
        gold: 50,
        ridium: 0
    }
)

const apple = new Classes.HealingItem("Apple", "./sprites/apple.sprite", 25);

hero.heal(apple);
hero.attack(hero);

This current code all works fine, but it seems like this current layout will cause issues in the future. Due to the Items.Type/Characters.Type

If this is the closest I can get to the result I would like, then...

In short...

I would like something like this

interface B {
    x: string
}

namespace A {
    export type v1 = number
    export type v2 = boolean
    export type v3 = B
}

let p: A = {
    v1: 9,
    v2: true,
    v3: { x: "some string" }
};
let q: A.v1 = 2;
let r: A.v2 = false;
let s: A.v3 = { x: "strung" };

This is not valid code as let p: A doesn't allow a namespace as a type, but hopefully this portrays what I am trying to achieve.

CodePudding user response:

If I understand correctly, you would like to be able to get rid of the .Type suffix, so that you could write directly:

class Character implements Characters {
  constructor(
    public name: string,
    public stats: Characters.Stats, // Type as a "member" of Characters
    public currency: Characters.Currency
  ) {}
  // Etc.
}

You are puzzled because you do not see how to "embed" Stats and Currency types as "members" of Characters.

On the one hand, if it were an interface, any "embedded" type would be seen as a member of the interface as well.

On the other hand, if it were a namespace, it could not be used directly as a type itself.

You can actually achieve both at the same time, because namespace enables merging into other types.

With this, we declare the interface, and we merge it with a namespace using the same name.

export interface Characters {
    name: string
    stats: Characters.Stats
    currency: Characters.Currency

    announce(sentence: string): void
    attack(target: Characters): void
    heal(item: HealingItem): void
}

// Merge the namespace with the type of the same name
namespace Characters {
    // Make sure the "embedded" types are explicitly exported
    export interface Stats {
        health: number
        attack: number
        defence: number
    }
    export interface Currency {
        gold: number
        ridium: number
    }
}

Playground Link


BTW, in your constructor, you do not need to explictly assign your class members if they are already used as constructor parameters with the same name and a visibility modifier, see Class Parameter Properties:

TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value. These are called parameter properties and are created by prefixing a constructor argument with one of the visibility modifiers public, private, protected, or readonly.

  • Related