Skip to content

Opaque type aliases  #15807

@Gozala

Description

@Gozala

Right now ts allow you create type aliases for primitives, which is nice but at the end of the day it's just a different name for the same type. It would be helpful to have a different way to alias scoped with in the module / namespace boundary. This would allow library author to export set of types that under the hood are aliases to the same primitive type, but for users of this library they are not, there for type checker will assist users & catch possible missuses.

Example

Consider example of using https://google.github.io/flatbuffers/ library. Conveniently it has a typescirpt generator which given following definition:

namespace Test;

table User {
 name:string;
}

I am posting type definitions of the relevant bits inline below, but you are interested you can see generated typescript output and full definition for the flatbuffers library

declare namespace flatbuffers {
  type Offset = number;
  
  class Builder {
    constructor(initial_size?: number);
    createString(s: string|Uint8Array): Offset;
    startObject(numfields: number): void;
    endObject():Offset;
  }
}

declare namespace Test {
  class User {
    static startUser(builder:flatbuffers.Builder):void;
    static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset):void;
    static endUser(builder:flatbuffers.Builder):flatbuffers.Offset;
  }
}

This is how user would create a Name table:

const builder = new flatbuffers.Builder()
const name:number = builder.createString('gozala')

Test.User.startUser(builder)
Test.User.addName(builder, name)
const user:number = Test.User.endUser(builder)

As you may notice both written strings and tables (as in fact every other data strucutre in flatbuffers) are represented via integer (that is an offset with in the buffer). This unfortunately means type checker won't be able to assist user with errors like the following:

Test.User.startUser(builder)
Test.User.addName(builder, user) // <- user is not a pointer to written string but rather to a table
const other:number = Test.User.endUser(builder)

On one hand flatbuffers library could box everything to provide a more type checker friendly APIs but that would come at the memory and execution cost, and in certain cases that is not good compromise.

Proposal

Now with proposed opaque type aliases here is how above inlined type difinitions could look like (I use Opaque<number> here but think of it as a placeholder for syntax you'd like to use instead):

declare namespace flatbuffers {
  type StringToken = Opaque<number>;
  type ObjectToken = Opaque<number>;
  
  class Builder {
    constructor(initial_size?: number);
    createString(s: string|Uint8Array): StringToken;
    startObject(numfields: number): void;
    endObject():ObjectToken;
  }
}

declare namespace Test {
  type UserToken = Opaque<flatbuffers.ObjectToken>;
  class User {
    static startUser(builder:flatbuffers.Builder):void;
    static addName(builder:flatbuffers.Builder, name:flatbuffers.StringToken):void;
    static endUser(builder:flatbuffers.Builder):UserToken;
  }
}

With that I would expect following behavior:

const builder = new flatbuffers.Builder()
const name = builder.createString('gozala')

Test.User.startUser(builder)
Test.User.addName(builder, 3) // <- Argument of type '3' is not assignable to parameter of type 'StringToken'.
  Test.User.addName(builder, name)
  
const user = Test.User.endUser(builder)

Test.User.startUser(builder)
Test.User.addName(builder, user) // <- Argument of type 'UserToken' is not assignable to parameter of type 'StringToken'.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions