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

Discriminant property type guard not applied with bracket notation #10530

Closed
jvilk opened this issue Aug 25, 2016 · 42 comments
Closed

Discriminant property type guard not applied with bracket notation #10530

jvilk opened this issue Aug 25, 2016 · 42 comments
Labels
Bug A bug in TypeScript Revisit An issue worth coming back to
Milestone

Comments

@jvilk
Copy link

jvilk commented Aug 25, 2016

TypeScript Version: 2.0.0

Code

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;
    }
}

Expected behavior:

The code compiles without errors.

Actual behavior:

sample.ts(24,33): error TS2339: Property 'size' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(24,42): error TS2339: Property 'size' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(25,36): error TS2339: Property 'width' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(25,46): error TS2339: Property 'height' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(26,43): error TS2339: Property 'radius' does not exist on type 'Square | Rectangle | Circle'.
sample.ts(26,54): error TS2339: Property 'radius' does not exist on type 'Square | Rectangle | Circle'.

Why this is bad:

I am trying to work with Dropbox's new 2.0 SDK, which heavily uses tagged union types (especially for API errors). The discriminant property is named .tag, so it can only be accessed via bracket notation. I generated TypeScript typings for their new JavaScript SDK, and discovered this bug the hard way. :(

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Aug 25, 2016
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 2.0.1 milestone Aug 25, 2016
@RyanCavanaugh
Copy link
Member

👍 Bracketed property access should work the same as dotted property access for the purposes of type guards when the indexed name matches a known property

@sandersn
Copy link
Member

Fix is up at #10565

@sandersn
Copy link
Member

Update from the PR at #10530: narrowing on element access, even just for strings and not numbers, adds 6.5% time to the checker, so this is not likely to go into 2.1 unless we come up with a way to reduce its cost.

@jvilk
Copy link
Author

jvilk commented Oct 17, 2016

@sandersn thanks for the update. That's unfortunate, but understandable. :(

@mhegazy mhegazy modified the milestones: Future, TypeScript 2.1 Oct 24, 2016
@GoToLoop
Copy link

... when the indexed name matches a known property.

Then I can rest assured that we can still continue to use unmatched index names?
So we can still hack the type by adding new properties using the [] syntax, right? 😨

jvilk pushed a commit to jvilk/stone that referenced this issue Jan 21, 2017
Adds a generator that produces TypeScript definition files for a JavaScript client. As with JavaScript, there are two generators: `tsd_types` and `tsd_client`.

The produced definition files require TypeScript 2.0 at the minimum, as they rely on TypeScript tagged unions.

Below, I will summarize how we map Stone types to TypeScript.

TypeScript's basic types match JSDoc, so there is no difference from the `js_types` generator.

Aliases are emitted as `type`s:

``` typescript
type AliasName = ReferencedType;
```

Structs are emitted as `interface`s, which support inheritance. Thus, if a struct `A` extends struct `B`, it will be emitted as:

``` typescript
interface A extends B {
  // fields go here
}
```

Nullable fields and fields with default values are emitted as _optional_ fields. In addition, the generator adds a field description with the default field value, if the field has one:

``` typescript
interface A {
  // Defaults to False
  recur?: boolean;
}
```

Unions are emitted as a `type` that is the disjunction of all possible union variants (including those from parent types!). Each variant is emitted as an individual `interface`.

```
union Shape
    point
    square Float64
        "The value is the length of a side."
    circle Float64
        "The value is the radius."
```

``` typescript
interface ShapeCircle {
  .tag: 'circle';
  circle: number;
}

interface ShapeSquare {
  .tag: 'square';
  square: number;
}

interface ShapePoint {
  .tag: 'point';
}

type Shape = ShapePoint | ShapeSquare | ShapeCircle;
```

TypeScript 2.0 supports [tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types) like these ones, so the compiler should automatically infer the type of a shape when the developer writes code like so (and statically check that all cases are covered!):

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
       // Compiler knows this is a ShapeSquare, so .square field is visible.
      console.log('square ' + shape.square);
      break;
    // No 'circle' case! If developer enables the relevant compiler option, compilation will fail.
}
```

Unfortunately, [there is a bug that prevents this from happening](microsoft/TypeScript#10530) when you use bracket notation to access a field. It will be fixed in TypeScript 2.1. Until then, developers will need to cast:

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
      console.log('square ' + (<ShapeSquare> shape).square);
      break;
}
```

When a struct explicitly enumerates its subtypes, direct references to the struct will have a `.tag` field to indicate which subtype it is. Direct references to the struct's subtypes will omit this field.

To capture this subtlety, the generator emits an interface that represents a direct reference to a struct with enumerated subtypes:

```
struct Resource
    union
        file File
        folder Folder

    path String

struct File extends Resource
    ...

struct Folder extends Resource
    ...
```

``` typescript
interface Resource {
  path: string;
}
interface File extends Resource {
}
interface Folder extends Resource {
}
interface ResourceReference extends Resource {
  '.tag': 'file' | 'folder';
}
interface FileReference extends File {
  '.tag': 'file';
}
interface FolderReference extends Folder {
  '.tag': 'folder';
}
```

Direct references to `Resource` will be typed as `FileReference | FolderReference | ResourceReference` if the union is open, or `FileReference | FolderReference` if the union is closed. A direct reference to `File` will be typed as `File`, since the `.tag` field will not be present.

TypeScript 2.0's tagged union support should work on these types once the previously-discussed bug is fixed.

Nullable types are emitted as optional fields when referenced from structs.

Routes are emitted in the same manner as the JavaScript generators, **except** that TypeScript's type system is unable to type `Promise`-based errors. The generator adds text to the route's documentation that explicitly mentions the data type the developer should expect when an error occurs.

Example:

``` typescript
type DropboxError = DropboxTypes.Error;
db.filesListFolder({path: ''}).then((response) => {
  // TypeScript knows the type of response, so no type annotation is needed.
}).catch(
  // Add explicit annotation on err.
  (err: DropboxError<DropboxTypes.files.ListFolderError>) => {

  });
```

Stone namespaces are mapped directly to TypeScript namespaces:

```
namespace files;

import common;

struct Metadata
    parent_shared_folder_id common.SharedFolderId?
```

``` typescript
namespace files {
  interface Metadata {
    parent_shared_folder_id?: common.SharedFolderId;
  }
}
```

Both `tsd_types` and `tsd_client` consume a template file, which contains a skeleton around the types they omit. This skeleton is unavoidable, as SDKs may augment SDK classes (like `Dropbox` or `DropboxTeam`) with additional methods not described in stone.

The "templates" simply have a comment string that marks where the generator should insert code. For example, the following template has markers for route definitions and type definitions:

``` typescript
class Dropbox {
  // This is an SDK-specific method which isn't described in stone.
  getClientId(): string;

  // All of the routes go here:
  /*ROUTES*/
}

// All of the stone data types are defined here:
/*TYPES*/
```

In the above template, the developer would need to run the `tsd_types` generator to produce an output file, and then run the `tsd_client` generator on that output to insert the routes (or vice-versa).

The developer may also choose to have separate template files for types and routes:

``` typescript
// in types.d.ts
namespace DropboxTypes {
  /*TYPES*/
}
```

``` typescript
/// <reference path="./types.d.ts" />
// ^ this will "import" the types from the other file.
// in dropbox.d.ts
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
}
```

Developers can customize the template string used for `tsd_client` with a command line parameter, in case they have multiple independent sets of routes:

``` typescript
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
  class DropboxTeam {
    /*TEAM_ROUTES*/
  }
}
```

For Dropbox's JavaScript SDK, I've defined the following templates.

**dropbox.d.tstemplate**: Contains a template for the `Dropbox` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
declare module DropboxTypes {
  class Dropbox extends DropboxBase {
    /**
     * The Dropbox SDK class.
     */
    constructor(options: DropboxOptions);

/*ROUTES*/
  }
}
```

**dropbox_team.d.tstemplate**: Contains a template for the `DropboxTeam` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
/// <reference path="./dropbox.d.ts" />
declare module DropboxTypes {
  class DropboxTeam extends DropboxBase {
    /**
     * The DropboxTeam SDK class.
     */
    constructor(options: DropboxOptions);

    /**
     * Returns an instance of Dropbox that can make calls to user api endpoints on
     * behalf of the passed user id, using the team access token. Only relevant for
     * team endpoints.
     */
    actAsUser(userId: string): Dropbox;

/*ROUTES*/
  }
}
```

**dropbox_types.d.ts**: Contains a template for the Stone data types, as well as the `DropboxBase` class (which is shared by both `Dropbox` and `DropboxTeam`).

``` typescript
declare module DropboxTypes {
  interface DropboxOptions {
    // An access token for making authenticated requests.
    accessToken?: string;
    // The client id for your app. Used to create authentication URL.
    clientId?: string;
    // Select user is only used by team endpoints. It specifies which user the team access token should be acting as.
    selectUser?: string;
  }

  class DropboxBase {
    /**
     * Get the access token.
     */
    getAccessToken(): string;

    /**
     * Get a URL that can be used to authenticate users for the Dropbox API.
     * @param redirectUri A URL to redirect the user to after authenticating.
     *   This must be added to your app through the admin interface.
     * @param state State that will be returned in the redirect URL to help
     *   prevent cross site scripting attacks.
     */
    getAuthenticationUrl(redirectUri: string, state?: string): string;

    /**
     * Get the client id
     */
    getClientId(): string;

    /**
     * Set the access token used to authenticate requests to the API.
     * @param accessToken An access token.
     */
    setAccessToken(accessToken: string): void;

    /**
     * Set the client id, which is used to help gain an access token.
     * @param clientId Your app's client ID.
     */
    setClientId(clientId: string): void;
  }

/*TYPES*/
}
```

Then, I defined simple definition files for each of the ways you package up the SDK, which references these types. These can be readily distributed alongside your libraries.

`DropboxTeam-sdk.min.d.ts` (`DropboxTeam` class in a UMD module):

``` typescript
/// <reference path="./dropbox_team.d.ts" />
export = DropboxTypes.DropboxTeam;
export as namespace DropboxTeam;
```

`Dropbox-sdk.min.d.ts` (`Dropbox` class in a UMD module):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
export as namespace Dropbox;
```

`dropbox-sdk.js` (`Dropbox` class in a CommonJS module -- not sure why you distribute this when you have a UMD version!):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
```

Finally, for your Node module, there's `src/index.d.ts` which goes alongside `src/index.js` and defines all of your Node modules together. After adding a `typings` field to `package.json` that points to `src/index`, the TypeScript compiler _automatically_ picks up the definitions from the NPM module:

``` typescript
/// <reference path="../dist/dropbox.d.ts" />
/// <reference path="../dist/dropbox_team.d.ts" />

declare module "dropbox/team" {
  export = DropboxTypes.DropboxTeam;
}

declare module "dropbox" {
  export = DropboxTypes.Dropbox;
}
```

To properly bundle things, I added a `typescript-copy.js` script that NPM calls when you run `npm run build`. The script simply copies the TypeScript typings to the `dist` folder.

These are the files that must be maintained to provide complete TypeScript typings for all of your distribution methods. The files that are likely to change in the future are the templates, as you add/modify/remove SDK-specific interfaces.
jvilk pushed a commit to jvilk/stone that referenced this issue Jan 21, 2017
Adds a generator that produces TypeScript definition files for a JavaScript client. As with JavaScript, there are two generators: `tsd_types` and `tsd_client`.

The definition files require TypeScript 2.0, as they rely upon TypeScript tagged unions.

Overview: Mapping Stone Types to TypeScript
===========================================

Below, I will summarize how we map Stone types to TypeScript.

Basic Types
-----------

TypeScript's basic types match JSDoc, so there is no difference from the `js_types` generator.

Alias
-----

Aliases are emitted as `type`s:

``` typescript
type AliasName = ReferencedType;
```

Struct
------

Structs are emitted as `interface`s, which support inheritance. Thus, if a struct `A` extends struct `B`, it will be emitted as:

``` typescript
interface A extends B {
  // fields go here
}
```

Nullable fields and fields with default values are emitted as _optional_ fields. In addition, the generator adds a field description with the default field value, if the field has one:

``` typescript
interface A {
  // Defaults to False
  recur?: boolean;
}
```

Unions
------

Unions are emitted as a `type` that is the disjunction of all possible union variants (including those from parent types!). Each variant is emitted as an individual `interface`.

```
union Shape
    point
    square Float64
        "The value is the length of a side."
    circle Float64
        "The value is the radius."
```

``` typescript
interface ShapeCircle {
  .tag: 'circle';
  circle: number;
}

interface ShapeSquare {
  .tag: 'square';
  square: number;
}

interface ShapePoint {
  .tag: 'point';
}

type Shape = ShapePoint | ShapeSquare | ShapeCircle;
```

TypeScript 2.0 supports [tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types) like these ones, so the compiler should automatically infer the type of a shape when the developer writes code like so (and statically check that all cases are covered!):

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
       // Compiler knows this is a ShapeSquare, so .square field is visible.
      console.log('square ' + shape.square);
      break;
    // No 'circle' case! If developer enables the relevant compiler option, compilation will fail.
}
```

Unfortunately, [there is a bug that prevents this from happening](microsoft/TypeScript#10530) when you use bracket notation to access a field. It will be fixed in a future version of TypeScript. Until then, developers will need to cast:

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
      console.log('square ' + (<ShapeSquare> shape).square);
      break;
}
```

Struct Polymorphism
-------------------

When a struct explicitly enumerates its subtypes, direct references to the struct will have a `.tag` field to indicate which subtype it is. Direct references to the struct's subtypes will omit this field.

To capture this subtlety, the generator emits an interface that represents a direct reference to a struct with enumerated subtypes:

```
struct Resource
    union
        file File
        folder Folder

    path String

struct File extends Resource
    ...

struct Folder extends Resource
    ...
```

``` typescript
interface Resource {
  path: string;
}
interface File extends Resource {
}
interface Folder extends Resource {
}
interface ResourceReference extends Resource {
  '.tag': 'file' | 'folder';
}
interface FileReference extends File {
  '.tag': 'file';
}
interface FolderReference extends Folder {
  '.tag': 'folder';
}
```

Direct references to `Resource` will be typed as `FileReference | FolderReference | ResourceReference` if the union is open, or `FileReference | FolderReference` if the union is closed. A direct reference to `File` will be typed as `File`, since the `.tag` field will not be present.

TypeScript 2.0's tagged union support should work on these types once the previously-discussed bug is fixed.

Nullable Types
--------------

Nullable types are emitted as optional fields when referenced from structs.

Routes
------

Routes are emitted in the same manner as the JavaScript generators, **except** that TypeScript's type system is unable to type `Promise`-based errors. The generator adds text to the route's documentation that explicitly mentions the data type the developer should expect when an error occurs.

Example:

``` typescript
type DropboxError = DropboxTypes.Error;
db.filesListFolder({path: ''}).then((response) => {
  // TypeScript knows the type of response, so no type annotation is needed.
}).catch(
  // Add explicit annotation on err.
  (err: DropboxError<DropboxTypes.files.ListFolderError>) => {

  });
```

Import / Namespaces
-------------------

Stone namespaces are mapped directly to TypeScript namespaces:

```
namespace files;

import common;

struct Metadata
    parent_shared_folder_id common.SharedFolderId?
```

``` typescript
namespace files {
  interface Metadata {
    parent_shared_folder_id?: common.SharedFolderId;
  }
}
```

Using the Generator
===================

Both `tsd_types` and `tsd_client` consume a template file, which contains a skeleton around the types they omit. This skeleton is unavoidable, as SDKs may augment SDK classes (like `Dropbox` or `DropboxTeam`) with additional methods not described in stone.

The "templates" simply have a comment string that marks where the generator should insert code. For example, the following template has markers for route definitions and type definitions:

``` typescript
class Dropbox {
  // This is an SDK-specific method which isn't described in stone.
  getClientId(): string;

  // All of the routes go here:
  /*ROUTES*/
}

// All of the stone data types are defined here:
/*TYPES*/
```

In the above template, the developer would need to run the `tsd_types` generator to produce an output file, and then run the `tsd_client` generator on that output to insert the routes (or vice-versa).

The developer may also choose to have separate template files for types and routes:

``` typescript
// in types.d.ts
namespace DropboxTypes {
  /*TYPES*/
}
```

``` typescript
/// <reference path="./types.d.ts" />
// ^ this will "import" the types from the other file.
// in dropbox.d.ts
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
}
```

Developers can customize the template string used for `tsd_client` with a command line parameter, in case they have multiple independent sets of routes:

``` typescript
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
  class DropboxTeam {
    /*TEAM_ROUTES*/
  }
}
```

Generator Usage in Dropbox SDK
==============================

For Dropbox's JavaScript SDK, I've defined the following templates.

**dropbox.d.tstemplate**: Contains a template for the `Dropbox` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
declare module DropboxTypes {
  class Dropbox extends DropboxBase {
    /**
     * The Dropbox SDK class.
     */
    constructor(options: DropboxOptions);

/*ROUTES*/
  }
}
```

**dropbox_team.d.tstemplate**: Contains a template for the `DropboxTeam` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
/// <reference path="./dropbox.d.ts" />
declare module DropboxTypes {
  class DropboxTeam extends DropboxBase {
    /**
     * The DropboxTeam SDK class.
     */
    constructor(options: DropboxOptions);

    /**
     * Returns an instance of Dropbox that can make calls to user api endpoints on
     * behalf of the passed user id, using the team access token. Only relevant for
     * team endpoints.
     */
    actAsUser(userId: string): Dropbox;

/*ROUTES*/
  }
}
```

**dropbox_types.d.ts**: Contains a template for the Stone data types, as well as the `DropboxBase` class (which is shared by both `Dropbox` and `DropboxTeam`).

``` typescript
declare module DropboxTypes {
  interface DropboxOptions {
    // An access token for making authenticated requests.
    accessToken?: string;
    // The client id for your app. Used to create authentication URL.
    clientId?: string;
    // Select user is only used by team endpoints. It specifies which user the team access token should be acting as.
    selectUser?: string;
  }

  class DropboxBase {
    /**
     * Get the access token.
     */
    getAccessToken(): string;

    /**
     * Get a URL that can be used to authenticate users for the Dropbox API.
     * @param redirectUri A URL to redirect the user to after authenticating.
     *   This must be added to your app through the admin interface.
     * @param state State that will be returned in the redirect URL to help
     *   prevent cross site scripting attacks.
     */
    getAuthenticationUrl(redirectUri: string, state?: string): string;

    /**
     * Get the client id
     */
    getClientId(): string;

    /**
     * Set the access token used to authenticate requests to the API.
     * @param accessToken An access token.
     */
    setAccessToken(accessToken: string): void;

    /**
     * Set the client id, which is used to help gain an access token.
     * @param clientId Your app's client ID.
     */
    setClientId(clientId: string): void;
  }

/*TYPES*/
}
```

Then, I defined simple definition files for each of the ways you package up the SDK, which references these types. These can be readily distributed alongside your libraries.

`DropboxTeam-sdk.min.d.ts` (`DropboxTeam` class in a UMD module):

``` typescript
/// <reference path="./dropbox_team.d.ts" />
export = DropboxTypes.DropboxTeam;
export as namespace DropboxTeam;
```

`Dropbox-sdk.min.d.ts` (`Dropbox` class in a UMD module):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
export as namespace Dropbox;
```

`dropbox-sdk.js` (`Dropbox` class in a CommonJS module -- not sure why you distribute this when you have a UMD version!):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
```

Finally, for your Node module, there's `src/index.d.ts` which goes alongside `src/index.js` and defines all of your Node modules together. After adding a `typings` field to `package.json` that points to `src/index`, the TypeScript compiler _automatically_ picks up the definitions from the NPM module:

``` typescript
/// <reference path="../dist/dropbox.d.ts" />
/// <reference path="../dist/dropbox_team.d.ts" />

declare module "dropbox/team" {
  export = DropboxTypes.DropboxTeam;
}

declare module "dropbox" {
  export = DropboxTypes.Dropbox;
}
```

To properly bundle things, I added a `typescript-copy.js` script that NPM calls when you run `npm run build`. The script simply copies the TypeScript typings to the `dist` folder.

These are the files that must be maintained to provide complete TypeScript typings for all of your distribution methods. The files that are likely to change in the future are the templates, as you add/modify/remove SDK-specific interfaces.
posita pushed a commit to dropbox/stone that referenced this issue Jan 22, 2017
Adds a generator that produces TypeScript definition files for a JavaScript client. As with JavaScript, there are two generators: `tsd_types` and `tsd_client`.

The definition files require TypeScript 2.0, as they rely upon TypeScript tagged unions.

Overview: Mapping Stone Types to TypeScript
===========================================

Below, I will summarize how we map Stone types to TypeScript.

Basic Types
-----------

TypeScript's basic types match JSDoc, so there is no difference from the `js_types` generator.

Alias
-----

Aliases are emitted as `type`s:

``` typescript
type AliasName = ReferencedType;
```

Struct
------

Structs are emitted as `interface`s, which support inheritance. Thus, if a struct `A` extends struct `B`, it will be emitted as:

``` typescript
interface A extends B {
  // fields go here
}
```

Nullable fields and fields with default values are emitted as _optional_ fields. In addition, the generator adds a field description with the default field value, if the field has one:

``` typescript
interface A {
  // Defaults to False
  recur?: boolean;
}
```

Unions
------

Unions are emitted as a `type` that is the disjunction of all possible union variants (including those from parent types!). Each variant is emitted as an individual `interface`.

```
union Shape
    point
    square Float64
        "The value is the length of a side."
    circle Float64
        "The value is the radius."
```

``` typescript
interface ShapeCircle {
  .tag: 'circle';
  circle: number;
}

interface ShapeSquare {
  .tag: 'square';
  square: number;
}

interface ShapePoint {
  .tag: 'point';
}

type Shape = ShapePoint | ShapeSquare | ShapeCircle;
```

TypeScript 2.0 supports [tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types) like these ones, so the compiler should automatically infer the type of a shape when the developer writes code like so (and statically check that all cases are covered!):

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
       // Compiler knows this is a ShapeSquare, so .square field is visible.
      console.log('square ' + shape.square);
      break;
    // No 'circle' case! If developer enables the relevant compiler option, compilation will fail.
}
```

Unfortunately, [there is a bug that prevents this from happening](microsoft/TypeScript#10530) when you use bracket notation to access a field. It will be fixed in a future version of TypeScript. Until then, developers will need to cast:

``` typescript
var shape: Shape = getShape();
switch (shape['.tag']) {
  case 'point':
      console.log('point');
      break;
   case 'square':
      console.log('square ' + (<ShapeSquare> shape).square);
      break;
}
```

Struct Polymorphism
-------------------

When a struct explicitly enumerates its subtypes, direct references to the struct will have a `.tag` field to indicate which subtype it is. Direct references to the struct's subtypes will omit this field.

To capture this subtlety, the generator emits an interface that represents a direct reference to a struct with enumerated subtypes:

```
struct Resource
    union
        file File
        folder Folder

    path String

struct File extends Resource
    ...

struct Folder extends Resource
    ...
```

``` typescript
interface Resource {
  path: string;
}
interface File extends Resource {
}
interface Folder extends Resource {
}
interface ResourceReference extends Resource {
  '.tag': 'file' | 'folder';
}
interface FileReference extends File {
  '.tag': 'file';
}
interface FolderReference extends Folder {
  '.tag': 'folder';
}
```

Direct references to `Resource` will be typed as `FileReference | FolderReference | ResourceReference` if the union is open, or `FileReference | FolderReference` if the union is closed. A direct reference to `File` will be typed as `File`, since the `.tag` field will not be present.

TypeScript 2.0's tagged union support should work on these types once the previously-discussed bug is fixed.

Nullable Types
--------------

Nullable types are emitted as optional fields when referenced from structs.

Routes
------

Routes are emitted in the same manner as the JavaScript generators, **except** that TypeScript's type system is unable to type `Promise`-based errors. The generator adds text to the route's documentation that explicitly mentions the data type the developer should expect when an error occurs.

Example:

``` typescript
type DropboxError = DropboxTypes.Error;
db.filesListFolder({path: ''}).then((response) => {
  // TypeScript knows the type of response, so no type annotation is needed.
}).catch(
  // Add explicit annotation on err.
  (err: DropboxError<DropboxTypes.files.ListFolderError>) => {

  });
```

Import / Namespaces
-------------------

Stone namespaces are mapped directly to TypeScript namespaces:

```
namespace files;

import common;

struct Metadata
    parent_shared_folder_id common.SharedFolderId?
```

``` typescript
namespace files {
  interface Metadata {
    parent_shared_folder_id?: common.SharedFolderId;
  }
}
```

Using the Generator
===================

Both `tsd_types` and `tsd_client` consume a template file, which contains a skeleton around the types they omit. This skeleton is unavoidable, as SDKs may augment SDK classes (like `Dropbox` or `DropboxTeam`) with additional methods not described in stone.

The "templates" simply have a comment string that marks where the generator should insert code. For example, the following template has markers for route definitions and type definitions:

``` typescript
class Dropbox {
  // This is an SDK-specific method which isn't described in stone.
  getClientId(): string;

  // All of the routes go here:
  /*ROUTES*/
}

// All of the stone data types are defined here:
/*TYPES*/
```

In the above template, the developer would need to run the `tsd_types` generator to produce an output file, and then run the `tsd_client` generator on that output to insert the routes (or vice-versa).

The developer may also choose to have separate template files for types and routes:

``` typescript
// in types.d.ts
namespace DropboxTypes {
  /*TYPES*/
}
```

``` typescript
/// <reference path="./types.d.ts" />
// ^ this will "import" the types from the other file.
// in dropbox.d.ts
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
}
```

Developers can customize the template string used for `tsd_client` with a command line parameter, in case they have multiple independent sets of routes:

``` typescript
namespace DropboxTypes {
  class Dropbox {
    /*ROUTES*/
  }
  class DropboxTeam {
    /*TEAM_ROUTES*/
  }
}
```

Generator Usage in Dropbox SDK
==============================

For Dropbox's JavaScript SDK, I've defined the following templates.

**dropbox.d.tstemplate**: Contains a template for the `Dropbox` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
declare module DropboxTypes {
  class Dropbox extends DropboxBase {
    /**
     * The Dropbox SDK class.
     */
    constructor(options: DropboxOptions);

/*ROUTES*/
  }
}
```

**dropbox_team.d.tstemplate**: Contains a template for the `DropboxTeam` class.

``` typescript
/// <reference path="./dropbox_types.d.ts" />
/// <reference path="./dropbox.d.ts" />
declare module DropboxTypes {
  class DropboxTeam extends DropboxBase {
    /**
     * The DropboxTeam SDK class.
     */
    constructor(options: DropboxOptions);

    /**
     * Returns an instance of Dropbox that can make calls to user api endpoints on
     * behalf of the passed user id, using the team access token. Only relevant for
     * team endpoints.
     */
    actAsUser(userId: string): Dropbox;

/*ROUTES*/
  }
}
```

**dropbox_types.d.ts**: Contains a template for the Stone data types, as well as the `DropboxBase` class (which is shared by both `Dropbox` and `DropboxTeam`).

``` typescript
declare module DropboxTypes {
  interface DropboxOptions {
    // An access token for making authenticated requests.
    accessToken?: string;
    // The client id for your app. Used to create authentication URL.
    clientId?: string;
    // Select user is only used by team endpoints. It specifies which user the team access token should be acting as.
    selectUser?: string;
  }

  class DropboxBase {
    /**
     * Get the access token.
     */
    getAccessToken(): string;

    /**
     * Get a URL that can be used to authenticate users for the Dropbox API.
     * @param redirectUri A URL to redirect the user to after authenticating.
     *   This must be added to your app through the admin interface.
     * @param state State that will be returned in the redirect URL to help
     *   prevent cross site scripting attacks.
     */
    getAuthenticationUrl(redirectUri: string, state?: string): string;

    /**
     * Get the client id
     */
    getClientId(): string;

    /**
     * Set the access token used to authenticate requests to the API.
     * @param accessToken An access token.
     */
    setAccessToken(accessToken: string): void;

    /**
     * Set the client id, which is used to help gain an access token.
     * @param clientId Your app's client ID.
     */
    setClientId(clientId: string): void;
  }

/*TYPES*/
}
```

Then, I defined simple definition files for each of the ways you package up the SDK, which references these types. These can be readily distributed alongside your libraries.

`DropboxTeam-sdk.min.d.ts` (`DropboxTeam` class in a UMD module):

``` typescript
/// <reference path="./dropbox_team.d.ts" />
export = DropboxTypes.DropboxTeam;
export as namespace DropboxTeam;
```

`Dropbox-sdk.min.d.ts` (`Dropbox` class in a UMD module):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
export as namespace Dropbox;
```

`dropbox-sdk.js` (`Dropbox` class in a CommonJS module -- not sure why you distribute this when you have a UMD version!):

``` typescript
/// <reference path="./dropbox.d.ts" />
export = DropboxTypes.Dropbox;
```

Finally, for your Node module, there's `src/index.d.ts` which goes alongside `src/index.js` and defines all of your Node modules together. After adding a `typings` field to `package.json` that points to `src/index`, the TypeScript compiler _automatically_ picks up the definitions from the NPM module:

``` typescript
/// <reference path="../dist/dropbox.d.ts" />
/// <reference path="../dist/dropbox_team.d.ts" />

declare module "dropbox/team" {
  export = DropboxTypes.DropboxTeam;
}

declare module "dropbox" {
  export = DropboxTypes.Dropbox;
}
```

To properly bundle things, I added a `typescript-copy.js` script that NPM calls when you run `npm run build`. The script simply copies the TypeScript typings to the `dist` folder.

These are the files that must be maintained to provide complete TypeScript typings for all of your distribution methods. The files that are likely to change in the future are the templates, as you add/modify/remove SDK-specific interfaces.
@mhegazy mhegazy added the Revisit An issue worth coming back to label May 22, 2017
@jcalz
Copy link
Contributor

jcalz commented Apr 1, 2023

If we're not going to close this, could someone edit the title and description to reflect the current purpose of the issue? Right now it's just generating noise in every issue closed as a duplicate of this one.

@MicahZoltu
Copy link
Contributor

As others have mentioned, this issue title/description isn't particularly clear, but from the issues marked as duplicate I'm guessing this issue is the same root cause as the following?

declare const apple: { [key: string]: boolean | undefined }
const banana: Record<string, boolean> = apple // error
// Type '{ [key: string]: boolean | undefined; }' is not assignable to type 'Record<string, boolean>'.
//   'string' index signatures are incompatible.
//     Type 'boolean | undefined' is not assignable to type 'boolean'.
//       Type 'undefined' is not assignable to type 'boolean'.(2322)

@joshkel
Copy link

joshkel commented Sep 12, 2023

@MicahZoltu I don't think that's the same issue, and I don't think the example that you shared would be considered an issue in TypeScript. Although a Record<string, boolean> and a Record<string, boolean | undefined> act very similarly, they differ in whether TS allows undefined to be explicitly assigned to properties, and they're consistently different in how TS does type checks of property values. See this playground link.

@RyanCavanaugh
Copy link
Member

Closing since the examples cited here work as expected

@DetachHead
Copy link
Contributor

@RyanCavanaugh in that case can any of the issues that were closed as a duplicate of this one be re-opened? Eg. #51368

@RyanCavanaugh
Copy link
Member

@DetachHead let's just get a fresh issue with a clear statement of the problem for clarity

@jcalz
Copy link
Contributor

jcalz commented Nov 14, 2023

Well, I opened #56389... not sure if it covers all the loose ends, though.

@phil294
Copy link

phil294 commented Oct 15, 2024

To anybody following this issue and confused like me, also the more complicated topic of variable index access type guards was fixed this June by ahejlsberg himself with #57847 in TS 5.5 :)

@DetachHead
Copy link
Contributor

the issue i raised (#51368) which was closed as a duplicate of this one still doesn't work:

interface Data {
    a?: number
}

declare const data: Data

declare let key: 'a'

if (data.a !== undefined) {
    key // "a"
    const a = data[key] // number | undefined
    const b = data['a'] // number
}

playground

the difference being that key is a let instead of a const. in #57847 it mentions that it should work on both const and let:

With this PR we perform control flow analysis for element access expressions obj[key] where key is a const variable, or a let variable or parameter that is never targeted in an assignment.

or is there something i'm missing?

@jcalz
Copy link
Contributor

jcalz commented Oct 16, 2024

You're missing that the let is in the global scope and so other files might affect it, see #58972 (comment)

@DetachHead
Copy link
Contributor

i see, thanks. it also seems like attempting it to narrow it with data.a then accessing it with data[key] also doesn't work, so i had to change that too:

+ export {}
+   
  interface Data {
      a?: number
  }
  
  declare const data: Data
  
  declare let key: 'a'
  
- if (data.a !== undefined) {
+ if (data[key] !== undefined) {
      key // "a"
      const a = data[key] // number | undefined
      const b = data['a'] // number
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Revisit An issue worth coming back to
Projects
None yet
Development

Successfully merging a pull request may close this issue.