pfooti
pfooti

Reputation: 2704

Type interface referencing a different Type's structure?

My goal is creating a type interface that has properties whose names are defined in a different interface.

I have the following definition for a model schema object in typescript.

export interface ModelSchema {
  idAttribute: string;
  name: string;
  attributes: {
    [attrName: string]:
      { type: 'number', default?: number, readOnly?: boolean} |
      { type: 'string', default?: string, readOnly?: boolean} |
      { type: 'boolean', default?: boolean, readOnly?: boolean} |
      { type: 'date', default?: Date, readOnly?: boolean} |
      { type: 'array', default?: string[] | number[], readOnly?: boolean } |
      { type: 'object', default?: object, readOnly?: boolean}
  };
  relationships: {
    [relName: string]: {
      type: RelationshipSchema,
      readOnly?: boolean,
    },
  };
};

The key bit for our discussion here is the relationships property. It can have any number of properties, each with a string name ('children', 'replies', etc), and each represents a has-many relationship in the datamodel.

I want to define a separate container for approvers - things the server can ask "It this user allowed to C/R/U/D that item / relationship?" The structure is parallel, since each relationship has different rules, each relationship will have its own approver method.

So, ideally, I'd say something like

export interface ApproverDefinition<S extends ModelSchema> {
  typeName: string,
  attributes: AttributesAuthorize,
  relationships: {
    [name: string]: RelationshipAuthorize,
  }
}

but then have a separate guard there where it basically says: "the string-indexed properties of ApproverDefinition.relationships need to have the same names as those of the relationships property of S, the ModelSchema that this is generically related to.

Something like (pseudocode here):

export interface ApproverDefinition<S extends ModelSchema> {
  typeName: string,
  attributes: AttributesAuthorize,
  relationships: {
    [name in keyof S.relationships]: RelationshipAuthorize,
  }
}

The above almost works (except that it doesn't require complete coverage, but I'll live with it)- I feel like it explains what I want to do, but I get two errors from TS. First, that I'm exporting S the private name on the interface, and second that I'm treating S like a namespace.

There's a number of reasons I need / want to keep the approver code distinct from the schema code, so just extending ModelSchema isn't really feasible here.

I can do run-time type checking, if I basically check to ensure that Object.keys(approver.relationships) and Object.keys(model.relationships) have the same entries, but I kind of want to capture this in typescript language.

Is it at all possible to do something like this?

EDIT: here's an example model schema:

const ProfileSchema = {
  idAttribute: 'id',
  name: 'profiles',
  attributes: {
    id: { type: 'number', readOnly: true },
    short_text: { type: 'string', readOnly: false },
    long_text: { type: 'string', readOnly: true },
    // SNIP ...
  },
  relationships: {
    memberships: { type: Memberships, readOnly: false },
    conversations: { type: ProfilePermissions, readOnly: false },
    followingProfiles: { type: FollowingProfiles, readOnly: false},
    followingDocuments: { type: FollowingDocuments, readOnly: false},
    followingCommunities: { type: FollowingCommunities, readOnly: false},
    followers: { type: FollowingProfiles, readOnly: false},
  },
};

and I'd like to define

const ProfileApprover: ApproverDefinition<ProfileSchema> = {
  typeName: 'profiles'
  attributes: /* attribute approver */
  relationships: {
    memberships: /* approver function */,
    conversations: /* approver function */,
    followingProfiles: /* approver function */,
    followingDocuments: /* approver function */,
    followingCommunities: /* approver function */,
    followers: /* approver function */,
  }
}

note that ProfileApprover.relationships has the same properties as ProfileSchema.relationships, but the properties have different values. This is what I'd like to specify in type rules - that all Approver instances match up with their corresponding schema.

I can always throw a runtime error when registering approvers if they're not there, but this feels like something that I should be able to statically define to typescript.

Upvotes: 0

Views: 140

Answers (1)

Aluan Haddad
Aluan Haddad

Reputation: 31873

If I understand you correctly, what you want is something like this

First we extract the type of the relationships property of ModelShema into an interface so that we can refer to it independently

export interface Relationships {
  [relName: string]: {
    type: RelationshipSchema,
    readOnly?: boolean,
  },
}

We use this interface in ModelSchema in place of the previous object literal type

export interface ModelSchema {
  idAttribute: string;
  name: string;
  attributes: {
    [attrName: string]:
    {type: 'number', default?: number, readOnly?: boolean} |
    {type: 'string', default?: string, readOnly?: boolean} |
    {type: 'boolean', default?: boolean, readOnly?: boolean} |
    {type: 'date', default?: Date, readOnly?: boolean} |
    {type: 'array', default?: string[] | number[], readOnly?: boolean} |
    {type: 'object', default?: object, readOnly?: boolean}
  };
  retationships: Relationships;
}

Since ApproverDefinition only references the relationship property of ModelSchema, we can just use its now available type as our constraint. This gives us access to the keys for use with keyof

export interface ApproverDefinition<R extends Relationships> {
  typeName: string;
  attributes: AttributesAuthorize;
  relationships: {
    [name in keyof R]: RelationshipAuthorize
  }
}

const ProfileSchema = {
  idAttribute: 'id',
  name: 'profiles',
  attributes: {
    id: {type: 'number', readOnly: true},
    short_text: {type: 'string', readOnly: false},
    long_text: {type: 'string', readOnly: true},
    // SNIP ...
  },
  relationships: {
    memberships: {type: Memberships, readOnly: false},
    conversations: {type: ProfilePermissions, readOnly: false},
    followingProfiles: {type: FollowingProfiles, readOnly: false},
    followingDocuments: {type: FollowingDocuments, readOnly: false},
    followingCommunities: {type: FollowingCommunities, readOnly: false},
    followers: {type: FollowingProfiles, readOnly: false},
  },
};

Finally, since ProfileSchema is a value, and not a type, we need to use typeof on the its relationships property and use the result as the type argument to ApproverDefinition

const ProfileApprover: ApproverDefinition<typeof ProfileSchema.relationships> = {
  typeName: 'profiles',
  attributes: {}, /* attribute approver */
  relationships: {
    // all of these are now required by the type
    memberships: /* approver function */,
    conversations: /* approver function */,
    followingProfiles: /* approver function */,
    followingDocuments: /* approver function */,
    followingCommunities: /* approver function */,
    followers: /* approver function */,
  }
};

Upvotes: 1

Related Questions