microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.32k stars 12.53k forks source link

Add feature to re-declare variable #37771

Open dguttman-jacada opened 4 years ago

dguttman-jacada commented 4 years ago

Search Terms

redeclare variable, re-declare variable, augment variable, merge variable declaration, declaration override

Suggestion

define a redeclare keyword which allows re-declaration of a typed variable. e.g.

redeclare var foo: {
  prop: string
};

Use Cases

When an external library declares a variable, e.g.

declare var x: {
  prop: string;
};

The redeclare keyword can be used somewhere else to override the declaration:

redeclare var x: number;

or augment it:

redeclare var x: typeof x & {
  augmentedProp: number;
}

This would allow to type the pure JavaScript case of:

// lib.js
var x = {
  prop: "foo"
};

// app.js
x.augmentedProp = 10;

console.log(x.prop, x.augmentedProp);

Examples

declare var URLSearchParams: {
    prototype: URLSearchParams;
    new(init?: string[][] | Record<string, string> | string | URLSearchParams): URLSearchParams;
    toString(): string;
};

defined in lib.dom.d.ts

to extend the variable with additional static methods to URLSearchParams is not possible currently (see here).

The feature can be used in the following way:

redeclare var URLSearchParams: typeof URLSearchParams & {
  fromObject(obj: object): URLSearchParams;
};

if (!URLSearchParams.fromObject) {
  URLSearchParams.fromObject = function(obj: object): URLSearchParams { /* implementation */  }
}

Checklist

My suggestion meets these guidelines:

DanielRosenwasser commented 4 years ago

I guess my question is what happens if two variables did this?

declare var hello: string | number;

redeclare var hello: number;
redeclare var hello: string;
dguttman-jacada commented 4 years ago

@DanielRosenwasser my answer would be:

  1. depending on context of execution flow . Even today, typescript knows that for:
    function foo(input: string | number): string {
     if (typeof input === "string") {
       return input; // here input is string
     } else {
       return number.toString(); // here input is number
     }
    } 

    so in your example, hello would end up a string (if the code is processed sequentially)

  2. It makes sense for the redeclare to be scoped, so if the variable was redeclared within a function body (or any other relevant scope), the redeclaration would be valid for the scope of the function body. e.g for an express app:

    app.get("/", (req, res, next) => {
     redeclare req: typeof req & { myProp: string };
     req.myProp = Guid.generate();
     asyncOp((res) => {
       res.send(res.req.myProp);
     });
    });
    
    req.myProp = ""; // error - req does not have a 'myProp' property
  3. in JavaScript it is also possible to do:

    // file1.js
    var hello = 123; // essentially declare it as a number, would work with declare var hello: string | number;
    
    // file2.js
    var hello = "foo"; // essentially declare it as a string, would work with declare var hello: string | number;
    
    // file3.js
    var hello = false; // essentially declare it as a boolean, would *not* work with the above and would need to be redeclared - in most cases: redeclare var hello: typeof hello | boolean;

    currently there is no way to describe this with TypeScript

ghost commented 3 years ago

Came here for a mildly different reason, closer related to issue https://github.com/microsoft/TypeScript/issues/14306, some variables were "declared" in the global scope, but I chose to explicitly make them into variables.

ex:

const { Map } = self;

const func  = () => {
    const variable = new Map{...};
    // Map cannot be overrode via devtools or such
}

Someone I was working with had actually noticed that global lookups were slowing down their code, so I tried to test this, but TS didn't like it.

jcalz commented 2 years ago

cross-linking to #4062

sdrsdr commented 2 years ago

redeclare can have other benefits:

I have a "header" type of interface:

export interface api_msg_t {
    a:string;
    devid:string;
}

function is_api_msg(m:any) :m is api_msg_t {
    return (
        m && typeof(m)=="object" && typeof(m.a)=='string' && typeof(m.devid)=='string'
    )
}

I want to have extended interface:

interface api_node_online_t extends api_msg_t {
    a:"onl";
    nid:number;
    state:node_state;
}
function is_api_node_online(m:api_msg_t) : m is api_node_online_t {
    return (m.a=='onl' && typeof(m.nid)=="number" && is_node_state(m.state));
}

if there is no inex type in api_msg_t the typeguard is_api_node_online fails to transpile as m.nid does not exist in api_msg_t. But adding index in the api_msg_t propagates it to api_node_online_t that is undesirable. if we can do

function is_api_node_online(m:api_msg_t) : m is api_node_online_t {
    redaclare m as api_msg_t & {[index:string]:unknonw};
    return (m.a=='onl' && typeof(m.nid)=="number" && is_node_state(m.state));
}

It will allow for elegant and typesafe solution.

We can easily mimic this behavior with

interface api_msg_indexed_t extends api_msg_t {
    [index:string]:nknown;
}

function is_api_msg_indexed(m:api_msg_t):m is api_msg_indexed_t:{
    return true;
}

function is_api_node_online(m:api_msg_t) : m is api_node_online_t {
    if (!api_msg_indexed(m)) return;
    return (m.a=='onl' && typeof(m.nid)=="number" && is_node_state(m.state));
}

but this results in unnecessary JS code emitted and ran