-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Placeholder Type Declarations #31894
Comments
What happens if no implementation type is found? Can we assign anything to a placeholder type reference? For example: exists type Buffer extends { toArray(): number[] }
export function printStuff(buff: Buffer) {
console.log(buff.toString());
}
printStufff({ toArray() { return [0] } }); // error here ? |
In your example only So to answer your question
|
I'm not a fan of exists module "net" {
exists type Server;
}
export function connect(srv: import("net").Server) {...} |
Alternatively:
|
@rbuckton I think you can already do that with module augmentations and ambient module declarations which merge. // globals.d.ts
declare module "net" {
export exists type Server;
}
// consumer.ts
import net = require("net");
function connect(srv: net.Server): {
// ...
} |
Two problems with that:
Just as the goal with I don't want to introduce a |
Aren’t they literally existential types though? You’re introducing an opaque type variable in an otherwise non-generic context, to stand in for any type it needs to be; (my understanding is) existential types do the same, allowing parameterization on a type without
Nevermind, I didn’t read it closely enough. It’s for forward declaration of types that will eventually be properly defined. That’s a different animal entirely. |
🏠🚲 Given that declare type Bar<T, U = number>;
declare type ManBearPig extends Man;
declare type ManBearPig extends Bear;
declare type ManBearPig extends Pig; Both of the above declarations are syntax errors today, so we can just appropriate |
@fatcerberus I like that idea of a |
There's an issue with not being able to supply the minimum expected definition. Let's say I have the following package "packageA": // tsconfig.json
{ "compilerOptions": { "target": "esnext", "lib": ["node"] } } // index.ts
// yes, this is a bad example, but bear with me...
declare global {
declare type Buffer;
}
export function copyBuffer(src: Buffer, dest: Buffer, srcStart: number,
destStart: number, count: number): void {
while (count > 0) {
dest.writeUint8(src.readUint8(srcStart++), destStart++);
count--;
}
} Now lets say I consume "packageA" from "packageB": // tsconfig.json
{ "compilerOptions": { "target": "esnext" } } import { copyBuffer } from "packageA";
declare global {
interface Buffer { iAmABuffer: boolean }
}
const src: Buffer = { iAmABuffer: true };
const dest: Buffer = { iAmABuffer: true };
copyBuffer(src, dest, 0, 0, 0); The package "packageA" isn't indicating to "packageB" where By specifying a constraint ( export function copyBuffer(
src: { readUint8(offset: number): number },
dest: { writeUint8(value: number, offset: number },
srcStart: number,
destStart: number,
count: number): void { ... } However this seems highly repetitive and overcomplicated. At the end of the day, what this boils down to is this:
So its not the case that just any In a way, I feel like this makes the whole // just some syntax bikeshedding...
exists type Server from "net" in package "@types/node";
exists type Buffer in package "@types/node";
exists type Promise in lib "es2015.promises"; In these cases, your project wouldn't need to have a dependency on "@types/node" or a
|
I still don’t see why we can’t just use |
Daniel explicitly calls out why just adding a |
Yes, I saw that bit. I specifically meant this: declare type Buffer;
// instead of exists type Buffer; Which is currently a syntax error. |
This feature reminds me of the |
@patrickroberts thanks for bringing up that example - I hadn't heard of weak symbols but it's conceptually very similar. The issue with |
@DanielRosenwasser I can't say I'd heard that term describing such objects before. Is that term used just within TypeScript internals, or should that term also have meaning to end users of TypeScript? |
It's not widely used, but it's been publicly explained enough, even within our release notes. I'm flexible, but I'd rather avoid |
This would be a really great addition to the language. I often want to use types and modules instead of classes, but the inability to hide the implementation of the type is annoying. Some examples where this would be useful in the wild are the file descriptor in Node's Reason and Ocaml have abstract types that are used in exactly this way. There's some more discussion in #321 which is for a similar request. EDIT Never mind. I misread the proposal. |
The forward type Foo; "Placeholder type" is also mentioned several times, so that might also be something to consider: placeholder type Foo; (I may be missing something, I only skimmed the proposal so I don't have a deep understanding of it yet) |
For syntax, I'm pretty fine with just [declare] type Foo; and [declare] type Foo extends Whatever; because it mirrors our other shorthand declare module "foo"; in which we just take the part of the declaration we do have (the name) and elide the body. |
Yes, having studied this proposal a bit deeper, |
Yes, that’s what I’ve been saying all along! 😉 |
Just thought I'd post my declaration-merging-as-placeholder-type experiment here. interface ExpectedConfigT {
x : number,
y : string,
toString () : string,
}
/**
* Your library
*/
interface ConfigT extends ExpectedConfigT {
__doesNotExist? : unknown;
}
declare function getX () : ConfigT["x"];
declare function getToString () : ConfigT["toString"];
/**
* Users of your library
*/
interface ConfigT {
x : 1337,
y : "hello",
toString(encoding?: string, start?: number, end?: number): string,
}
/**
* Type is `1337`
*/
const x = getX();
/**
* Type is `(encoding?: string | undefined, start?: number | undefined, end?: number | undefined) => string`
*/
const toStringFunc = getToString(); |
## Description Typescript includes types even if they're not explicitly imported. This behavior makes some declaration types in Gesture Handler include node's typings which isn't desired since React Native shadows some declarations from Node (e.g. [`setInterval`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d14f07228ede33b23717f978c1f395d9570653cf/types/react-native/globals.d.ts#L12)). We're utilizing [`types`](https://www.typescriptlang.org/tsconfig#types) property to prevent TS from including all definitions from `node_modules/@types`. Hopefully, finally fixes #1384 ### References - [Discussion about _excluding_ some packages' definitions and TS maintainer response](microsoft/TypeScript#18588 (comment)) - [PR in TS introducing opaque type definitions](microsoft/TypeScript#31894) - this can prevent those type of issues in the future
I had a very similar idea to solve the same problem. What if, instead of checking if a type is declared globally, a type can be marked optional. If a type is marked optional, TypeScript will try to resolve this, but if it fails to do so, then it defaults to A practical trimmed down example based on optional type Buffer = Buffer;
export default function postcss(css: string | Buffer): any; If NodeJS types are includes, this is equal to: type Buffer = Buffer;
export default function postcss(css: string | Buffer): any; otherwise, this is equal to: type Buffer = never;
export default function postcss(css: string | Buffer): any; A practical trimmed down example based on optional type HTTPAgent = import('http').Agent;
optional type HTTPSAgent = import('https').Agent;
interface RequestConfig {
httpAgent: httpAgent;
httpsAgent: httpsAgent;
}
export default function request(config: RequestConfig): any; If NodeJS types are includes, this is equal to: type HTTPAgent = import('http').Agent;
type HTTPSAgent = import('https').Agent;
interface RequestConfig {
httpAgent: httpAgent;
httpsAgent: httpsAgent;
}
export default function request(config: RequestConfig): any; otherwise, this is equal to: type HTTPAgent = never;
type HTTPSAgent = never;
interface RequestConfig {
httpAgent: httpAgent;
httpsAgent: httpsAgent;
}
export default function request(config: RequestConfig): any; At first I thought of using |
It's a bit of a hack, but you can sort of do your Agent example today. Try // @ts-ignore
import type {Agent as HTTPAgent} from "http";
// @ts-ignore
import type {Agent as HTTPSAgent} from "https";
interface RequestConfig {
httpAgent: any extends HTTPAgent ? never : HTTPAgent;
httpsAgent: any extends HTTPSAgent ? never : HTTPSAgent;
} The ts-ignore comment causes the type to resolve as I believe you can actually do the same thing with your other examples if you just find the modules that Maybe we just need a shorthand to do this without abusing ignore-comments? I don't know what the syntax would look like, but I'd love to have an import statement that says "give me type X from package Y if it's installed, otherwise FallbackType" (where I get to pick FallbackType). ETA: You may find microsoft/TypeScript-DOM-lib-generator#1207 interesting, as right now I don't think there's a way to use this workaround with DOM types. |
I keep getting feedback on this related SO question, and reading back over the thread here for more context. One thing I don't think has been discussed: does this proposal help when your code needs to return a placeholder type? declare type Buffer {
// ...?
}
declare type Socket {
on(evt: "data", cb: (b:Buffer) => void): void;
}
export function doStuff(input: string): Promise<string>;
export function doStuff(input: Socket): Promise<Buffer>;
export function doStuff(input: string | Socket): Promise<string|Buffer> {
if (isSocket(input)) {
const bufferPromise = ...;
input.on("data", ...);
return bufferPromise;
} else {
return browserDoStuff(input);
}
} For isomorphic libraries, the idea is that the |
You're asking whether a local placeholder with the name In that case, no, and that was part of the concern I brought up over in #31894 (comment)
Last I checked, |
Thanks, Daniel. Just to be clear, is the answer different for imported versus global types? Like, if I build my library without |
Background
There are times when users need to express that a type might exist depending on the environment in which code will eventually be run. Typically, the intent is that if such a type can be manufactured, a library can support operations on that type.
One common example of this might be the
Buffer
type built into Node.js. A library that operates in both browser and Node.js contexts can state that it handles aBuffer
if given one, but the capabilities ofBuffer
aren't important to the declarations.One technique to get around this is to "forward declare"
Buffer
with an empty interface in the global scope which can later be merged.For consuming implementations, a user might need to say that a type not only exists, but also supports some operations. To do so, it can add those members appropriately, and as long as they are identical, they will merge correctly. For example, imagine a library that can specially operate on HTML DOM nodes.
A user might be running in Node.js or might be running with
"lib": ["dom"]
, so our implementation can forward-declareHTMLElement
, while also declaring that it containsinnerText
.Issues
Using interface merging works okay, but it has some problems.
Conflicts
Interface merging doesn't always correctly resolve conflicts between declarations in two interfaces. For example, imagine two declarations of
Buffer
that merge, where a function that takes aBuffer
expects it to have atoString
property.If both versions of
toString
are declared as a method, the two appear as overloads which is slightly undesirable.Alternatively, if any declaration of
toString
is a simple property declaration, then all other declarations will be considered collisions which will cause errors.The former is somewhat undesirable, and the latter is unacceptable.
Limited to Object Types
Another problem with the trick of using interfaces for forward declarations is that it only works for classes and interfaces. It doesn't work for, say, type aliases of union types. It's important to consider this because it means that the forward-declaration-with-an-interface trick breaks as soon as you need to convert an interface to a union type. For example, we've been taking steps recently to convert
IteratorResult
to a union type.Structural Compatibility
An empty interface declaration like
allows assignment from every type except for
unknown
,null
, andundefined
, because any other type is assignable to the empty object type ({}
).Proposal
Proposed is a new construct intended to declare the existence of a type.
A placeholder type declaration acts as a placeholder until a type implementation is available. It provides a type name in the current scope, even when the concrete implementation is unknown. When a non-placeholder declaration is available, all references to that type are resolved to an implementation type.
The example given is relatively simple, but placeholder types can also support constraints and type parameters.
A formal grammar might appear as follows.
Implementation Types
A placeholder type can co-exist with what we might call an implementation type - a type declared using an interface, class, or type alias with the same name as the placeholder type.
In the presence of an implementation type, a placeholder defers to that implementation. In other words, for all uses of a type name that references both a placeholder and an implementation, TypeScript will pretend the placeholder doesn't exist.
Upper Bound Constraints
A placeholder type is allowed to declare an upper bound, and uses the same syntax as any other type parameter constraint.
This allows implementations to specify the bare-minimum of functionality on a type.
If a constraint isn't specified, then the upper bound is implicitly
unknown
.When an implementation type is present, the implementation is checked against its constraint to see whether it is compatible. If not, an implementation should issue an error.
Type Parameters
A placeholder type can specify type parameters. These type parameters specify a minimum type argument count for consumers, and a minimum type parameter count for implementation types - and the two may be different!
For example, it is perfectly valid to specify only type arguments which don't have defaults at use-sites of a placeholder type.
But an implementation type must declare all type parameters, even default-initialized ones.
Whenever multiple placeholder type or implementation type declarations exist, their type parameter names must be the same.
Different instantiations of placeholders that have type parameters are only related when their type arguments are identical - so for the purposes of variance probing, type parameters are considered invariant unless an implementation is available.
Relating Placeholder Types
Because placeholder types are just type variables that recall their type arguments, relating placeholders appears to fall out from the existing relationship rules.
The intent is
In effect, two rules in any of our type relationships should cover this:
Merging Declarations
Because different parts of an application may need to individually declare that a type exists, multiple placeholder types of the same name can be declared, and much like
interface
declarations, they can "merge" in their declarations.In the event that multiple placeholder types merge, every corresponding type parameter must be identical. On the other hand, placeholder constraints can all differ.
When multiple placeholder types are declared, their constraints are implicitly intersected to a single upper-bound constraint. In our last example,
ManBearPig
's upper bound is effectivelyMan & Bear & Pig
. In our first example withBeetlejuice
, the upper bound isunknown & unknown & unknown
which is justunknown
.Prior Art
C and C++ also support forward declarations of types, and is typically used for opaque type handles. The core idea is that you can declare that a type exists, but can never directy hold a value of that type because its shape/size is never known. Instead, you can only deal with pointers to these forward declared types.
This allows APIs to abstract away the shape of forward-declared types entirely, meaning that the size/shape can change. Because these can only be pointers, there isn't much you can do with them at all (unlike this implementation).
Several other programming languages also support some concept of "opaque" or "existential" types, but are generally not used for the same purposes. Java has wildcards in generics, which is typically used to allow one to say only a bit about how a collection can be used (i.e. you can only write
Foo
s to some collection, or readBar
s, or you can do absolutely nothing with the elements themselves). Swift allows return types to be opaque in the return type by specifying that it is returningsome SuperType
(meaning some type variable that extendsSuperType
).FAQ and Rationale
Why can placeholder types have multiple declarations with different constraints?
We have two "obvious" options.
I believe that additive constraints are the more desirable behavior for a user. The idea is that different parts of your application may need different capabilities, and given that
interface
s can already model this with interface merging, using intersections provides a similar mechanism.Are these just named bounded existential types?
In part, yes! When no implementation type exists, a placeholder type acts as a bounded existential type variable.Sorry I'm not sure what you're talking about. Please move along and don't write blog posts about how TypeScript is adding bounded existential types.
Can placeholder types escape scopes?
Maybe! It might be possible to disallow placeholder types from escaping their declaring scope. It might also be reasonable to say that a placeholder can only be declared in the top level of a module or the global scope.
Do we need the
exists
keyword?Maybe we don't need the
exists
keyword - I am open to doing so, but wary that we are unnecessarily abusing the same syntax. I'd prefer to be explicit that this is a new concept with separate syntax, but if we did drop theexists
, we would change the grammar to the following.The text was updated successfully, but these errors were encountered: