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

Feature request: type-only members #18556

Closed
magnushiie opened this issue Sep 18, 2017 · 15 comments
Closed

Feature request: type-only members #18556

magnushiie opened this issue Sep 18, 2017 · 15 comments
Labels
Duplicate An existing issue was already created

Comments

@magnushiie
Copy link
Contributor

The following pattern allows one to define run-time structures that also have a compile-time representation:

type FieldMetadata = {}; // omitted;
type Field<T> = {
  instance: T;
  metadata: FieldMetadata;
};
  
export type QuerySpec<D extends {[P in keyof D]: Field<any>}> = {[P in keyof D]: D[P]["instance"]};

However, because Field<T> instances would be effectively singletons, the instance field is never going to be filled in because it's only used for compile-time type checking/inference.

Therefore, I propose to have "type-only" members, e.g.:

readonly inspiration

type Field<T> = {
  typeonly instance: T;
  metadata: FieldMetadata;
};

This would mean that accessing the instance member in an expression context would result in an error (only type indexing is allowed).

member types

type Field<T> = {
  type InstanceType = T;
  metadata: FieldMetadata;
};
@mhegazy
Copy link
Contributor

mhegazy commented Sep 18, 2017

why not just use a name that does not show up in completion list, e.g. something that starts with a non-identifier character:

type Field<T> = {
	"#instance": T;
	metadata: FieldMetadata;
};

@magnushiie
Copy link
Contributor Author

This would invent non-standard syntax for something that would fool the IDE but not the compiler, field["#instance"] would still be a valid expression providing a valid T.

Also, consider if TypeScript ever added a definite assignment rule - then this would have to be hacked around with undefined as any as T in the constructor.

@magnushiie
Copy link
Contributor Author

To clarify - there is no valid value for the instance field of Field<T> - as Field<T> is singleton, some choices of the instance field could only be an undefined unsafely cast to T or some random example/sentinel value of T.

So instead of

const PersonNameField = new Field<string>({fieldName: "name"});

one would have to use

const PersonNameField = new Field<string>({fieldName: "name", instance: "John Smith"});

where the value "John Smith" would never be actually used.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 18, 2017

can you elaborate on the issue. not sure why you need to pass in an extra parameter?

@magnushiie
Copy link
Contributor Author

magnushiie commented Sep 18, 2017

The idea is to define a schema using expressions, not types, in a DSL. This way the types are inferred and there is no repetition. Also, sometimes the schema might contain more specific information than the TypeScript type system (e.g. field lengths, number types, etc).

E.g.

namespace framework {
  type Field<T> = {
    name: string;
    typeName: string;
    instance: T;
  };
  type FieldSchema<D> = {[P in keyof D]: Field<any>};
  type Schema<D extends FieldSchema<D>> = { fieldSchema: D };
  export const makeSchema = <D extends FieldSchema<D>>(fieldSchema: D): Schema<D> => ({ fieldSchema: fieldSchema });
  type FieldSchemaOfSchema<S extends Schema<any>> = S["fieldSchema"];
  type InstanceTypeOfField<F extends Field<any>> = F["instance"];
  export type SchemaType<S extends Schema<any>> = {[P in keyof FieldSchemaOfSchema<S>]: InstanceTypeOfField<FieldSchemaOfSchema<S>[P]>};
  export const StringField: (name: string) => Field<string> = name => ({ name, instance: undefined as any as string, typeName: "string" });
  export const IntField: (name: string) => Field<number> = name => ({ name, instance: undefined as any as number, typeName: "int" });
}
namespace schema {
  export const PersonSchema = framework.makeSchema({
    firstName: framework.StringField("firstName"),
    lastName: framework.StringField("lastName"),
    age: framework.IntField("age"),
  });
  export type Person = framework.SchemaType<typeof PersonSchema>;
}

namespace code {
  console.log(JSON.stringify(schema.PersonSchema));
  const person: schema.Person = {
    firstName: "Magnus",
    lastName: "Hiie",
    age: 40 // "", // Types of property 'age' are incompatible. Type 'string' is not assignable to type 'number'.
  };
  console.log(JSON.stringify(person));
}

produces:

{"fieldSchema":{"firstName":{"name":"firstName","typeName":"string"},"lastName":{"name":"lastName","typeName":"string"},"age":{"name":"age","typeName":"int"}}}
{"firstName":"Magnus","lastName":"Hiie","age":40}

Note the undefined as any as casts in the framework to produce an unspecified example value.

EDIT: simplified a little

@jcalz
Copy link
Contributor

jcalz commented Sep 19, 2017

Is this related to or a duplicate of #17588?

@jcalz
Copy link
Contributor

jcalz commented Sep 19, 2017

I agree that it would be nice to do this without adding a phantom property which is not intended to exist at runtime. Currently if you ever need an instance of the augmented type, you either have to make the property optional (which dilutes T into T | undefined), or make it required and add a dummy void 0 as any value (which becomes obnoxious for object literals).

@magnushiie
Copy link
Contributor Author

Is this related to or a duplicate of #17588?

Yes, thank you. And #9889 which it links to.

@gcanti
Copy link

gcanti commented Sep 19, 2017

The idea is to define a schema using expressions, not types, in a DSL

@magnushiie you may want to take a look at

@magnushiie
Copy link
Contributor Author

Thanks, @gcanti, I will take a look. My use case is actually a little bit more specific, but maybe I can get some ideas.

@magnushiie
Copy link
Contributor Author

Both of these projects also have these "phantom" properties:

https://github.com/pelotom/runtypes has _falseWitness

https://github.com/gcanti/io-ts has _A

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 19, 2017

This might be something that could tie into associated types (?)

@magnushiie
Copy link
Contributor Author

This issue is especially bad where the false witnesses make the types unnecessarily covariant on the generic argument, whereas a type-only member or typedef would not impose covariance on the type.

E.g. the following interface is now invariant on T:

interface DoSomethingWith<T> {
  public readonly whatCanIDoSomethingWithFalseWitness: T;
  public doSomething(something: T) {
  }
}

@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

Duplicate of #17588

@mhegazy mhegazy marked this as a duplicate of #17588 Jul 17, 2018
@mhegazy mhegazy added the Duplicate An existing issue was already created label Jul 17, 2018
@typescript-bot
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants