Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opaque type aliases #15807

Closed
Gozala opened this issue May 12, 2017 · 2 comments
Closed

Opaque type aliases #15807

Gozala opened this issue May 12, 2017 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@Gozala
Copy link

Gozala commented May 12, 2017

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'.
@mhegazy
Copy link
Contributor

mhegazy commented May 12, 2017

Today you can do this using tags, e.g. type StringToken = number & {_tag: "__GUID__"}, more about this in #4895

A cleaner solution as you noted is to have some types treated as nominal, that is tracked by #202.

@mhegazy mhegazy added the Duplicate An existing issue was already created label May 12, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 30, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed May 30, 2017
chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 6, 2018
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给
不关心业务逻辑而只是简单接受 string 的底层组件时,需要通过 `as string`
强转,如果该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,
结果丢失所有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型
参数(泛型声明),结果增加冗长而意义不大的泛型声明。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - microsoft/TypeScript#15807
 - microsoft/TypeScript#4895
 - microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54
chuan6 added a commit to teambition/teambition-sdk that referenced this issue Jun 6, 2018
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。

不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给不关心业务逻
辑而只是简单接受 string 的底层组件时,需要通过 `as string` 强转,如果
该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,结果丢失所
有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型参数(泛型
声明),结果增加冗长而意义不大的泛型声明。

而不满足3,会漏掉很多类型检查,因为并不是任何 string 类型的值都可以赋
值给特定 id 类型的。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - microsoft/TypeScript#15807
 - microsoft/TypeScript#4895
 - microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants