Skip to content

Variable merging #4062

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

Open
ArtemAvramenko opened this issue Jul 28, 2015 · 9 comments
Open

Variable merging #4062

ArtemAvramenko opened this issue Jul 28, 2015 · 9 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@ArtemAvramenko
Copy link

declare var test: (s: string) => void;

declare var test: {
    (s: string) => void;
    tests: any[];
}

At the moment TS compiler shows error: Subsequent variable declarations must have the same type.

@RyanCavanaugh
Copy link
Member

What's the intent of this code?

@ArtemAvramenko
Copy link
Author

First declaration is from nuget package. I do not want to modify it.

@danquirk
Copy link
Member

We've had this request before. The problem is how do we support this dynamic, expando type of var usage but disallow meaningful errors that people generally associate with a good static checker. For example, you presumably would not have wanted to be allowed to declare var test: number; here, that would surely have been/caused a bug. But perhaps if the subsequent declaration is a subtype of the previous declaration we could allow it?

@danquirk danquirk changed the title Declaration Merging for Functions and Interfaces Variable merging Jul 29, 2015
@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jul 29, 2015
@ArtemAvramenko
Copy link
Author

It would be nice if it will work the same way as in the case of interface merging. I.e. interpret a type of the variable, as a declaration of an anonymous interface:

declare interface _anonymous_interface_for_global_var_test: {
    (s: string) => void;
}

declare interface _anonymous_interface_for_global_var_test: {
    (s: string) => void;
    tests: any[];
}

@yahiko00
Copy link

yahiko00 commented Aug 7, 2015

If we allow this, it seems to imply we should allow extension of existing union types. But maybe I am wrong.

@ghost
Copy link

ghost commented Jan 25, 2018

It might be useful to support this in the particular case of a property of type any. Often DefinitelyTyped declarations will declare something to be of type any when a particular user will know a better type.

// DefinitelyTyped
interface I { foo: any; }

// My code
interface I { foo: { x: number }; }

Currently we give an error because the types for foo conflict. We might just throw out any declarations typed as any when getting the type.

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Jul 3, 2018

Just ran into this again trying to define the type of state in history's Location, which is defined as any in the official typings, and there's no provided means of overriding it: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/566e0394859fdc1dc893658ccec6b06372d56a91/types/history/index.d.ts#L46

DefinitelyTyped/DefinitelyTyped#27012

@sdrsdr
Copy link

sdrsdr commented Oct 13, 2022

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

@RobertAKARobin
Copy link

Here is another example. Let's say we're making a library of utility functions. We want to import some of the utility functions into a front-end project.

To keep things organized, we want the utility functions to always be namespaced. For example, we want to require our developers to call utils.foo() and utils.bar() instead of just foo() and bar().

(The inspiration is the popular library Rxjs, which exports functions with very generic names like from and find, which when not namespaced can make code confusing.)

Simplest but worst solution

The library could have an index file that exports all the helper functions, which can then be imported with *, e.g.:

import * as utils from `utils/index.ts`;

utils.foo();
utils.bar();

However, without some very aggressive tree-shaking this would add all the helper functions into the project's Javascript bundle, which would be a lot of unnecessary bloat if the project only actually uses a few of the functions.

Mediocre solution

The project could have a file that is an interface between the project and the library, in which we import and export just the functions we want to use in the project:

// src/utils.ts

export { foo } from 'utils/foo.ts';
export { bar } from 'utils/bar.ts';
// src/app.ts

import * as utils from 'src/utils.ts';

utils.foo();
utils.bar();

However, this means someone has to maintain that interface, and if the project grows to encompass many modules with separate JS bundles, we could run into the same bloat problem as before.

Other mediocre solution

We could try to enforce a style rule that the function names have to be prefixed:

// src/app.ts

import { foo as utilsFoo } from 'utils/foo.ts';
import { bar as utilsBar } from 'utils/bar.ts';

...but that's annoying boilerplate to write, and is hard to enforce.

What we'd like

We'd like to (a) import just the modules we want, while (b) preserving namespacing.

We can declare additional global properties by adding them to the globalThis object like so:

declare global {
	interface HTMLElementEventMap {
		myCustomEvent: CustomEvent;
	}
}

It would be nice if there was a way to do something similar with other objects. To my mind it would look something like this:

// util/index.ts

export const utils = {};
// util/foo.ts

function foo() {};

utils.foo = foo;

declare module utils {
  export const foo = typeof foo;
}
// util/bar.ts

function bar() {};

utils.bar = bar;

declare module utils {
  export const bar = typeof bar;
}
// src/app.ts

import { utils } from 'util/index.ts';
import 'util/foo.ts'; // This causes `utils.foo` to be defined

utils.foo();
utils.bar(); // Error: undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants