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

Compile-time checking of string literal arguments based on type #394

Closed
jbrantly opened this Issue Aug 7, 2014 · 15 comments

Comments

Projects
None yet
7 participants
@jbrantly

jbrantly commented Aug 7, 2014

Often a method will take as a string the name of a property of some object that it's working on. A common example would be http://backbonejs.org/#Model-get

It would be ideal if TypeScript could provide compile-time checking on these calls to make sure that the string is actually a valid property of the underlying model. As a possible proposal:

interface Person {
    firstName: string
    lastName: string
    phoneNumber: string
}

function get(property: memberof Person) {}

get('firstName') // compiles
get('middleName') // would not compile

In the context of the get function, "memberof Person" would be equivalent to "string".

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 7, 2014

Member

Overload on constants already accomplishes this to some degree, although its purpose is mostly about type flow rather than errors on specific strings. The question is do you expect this to be an error:

var fieldNameFromUI = askUserForInput(); // user types 'name'
get(fieldNameFromUI); // is this an error now?

Are you satisfied only getting errors for string literal argument values and never when you pass string typed variables to this function? This is how overload on constant signatures work today. Alternatively they could be changed to not require a general overload that takes a string, but then it would always be an error to pass a variable instead of a string literal which we thought was not very desirable. To do better than that we'd most likely need to implement some sophisticated data flow analysis to try to understand what the literal value in a string variable is (and it would still fail to figure out the value with some frequency).

Member

danquirk commented Aug 7, 2014

Overload on constants already accomplishes this to some degree, although its purpose is mostly about type flow rather than errors on specific strings. The question is do you expect this to be an error:

var fieldNameFromUI = askUserForInput(); // user types 'name'
get(fieldNameFromUI); // is this an error now?

Are you satisfied only getting errors for string literal argument values and never when you pass string typed variables to this function? This is how overload on constant signatures work today. Alternatively they could be changed to not require a general overload that takes a string, but then it would always be an error to pass a variable instead of a string literal which we thought was not very desirable. To do better than that we'd most likely need to implement some sophisticated data flow analysis to try to understand what the literal value in a string variable is (and it would still fail to figure out the value with some frequency).

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Aug 7, 2014

Definitely not looking for any sort of run-time checking. As far as data flow analysis, ex:

var fieldName = 'firstName';
get(fieldName)

I can see that being tricky and personally would not mind if that did not throw any error. I'm not sure if I personally have an opinion on whether or not variables would be allowed at all. I think my leaning would be "yes, variables are allowed". The main issue that I'm trying to solve is that in certain cases you absolutely have to specify a string to access a property and it would be nice if in those cases you could get some compile-time checking so that refactoring/etc is less scary.

jbrantly commented Aug 7, 2014

Definitely not looking for any sort of run-time checking. As far as data flow analysis, ex:

var fieldName = 'firstName';
get(fieldName)

I can see that being tricky and personally would not mind if that did not throw any error. I'm not sure if I personally have an opinion on whether or not variables would be allowed at all. I think my leaning would be "yes, variables are allowed". The main issue that I'm trying to solve is that in certain cases you absolutely have to specify a string to access a property and it would be nice if in those cases you could get some compile-time checking so that refactoring/etc is less scary.

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 7, 2014

Member

So in that case if we altered the requirements for overload on constant signatures you could get some of this without changing much. Today:

function get(property: 'firstName');
function get(property: 'lastName');
function get(property: 'phoneNumber');
function get(property: string); // this is a required signature today
function get(property: string) {
    // do the getting
}
var result = get('firstName'); // ok
var result2 = get('somethingElse'); // ok
var x = 'hi';
var result3 = get(x); // ok

Alternate reality:

function get(property: 'firstName');
function get(property: 'lastName');
function get(property: 'phoneNumber');
//function get(property: string); // this is no longer required
function get(property: string) {
    // do the getting
}
var result = get('firstName'); // ok
var result2 = get('somethingElse'); // error
var x = 'hi';
var result3 = get(x); // ok

It's a little more verbose to declare than your 'memberof' proposal but it does give you that error checking. It's not clear how often you'd be able to define an API like this that only ever used string literals and never string typed variables though.

Member

danquirk commented Aug 7, 2014

So in that case if we altered the requirements for overload on constant signatures you could get some of this without changing much. Today:

function get(property: 'firstName');
function get(property: 'lastName');
function get(property: 'phoneNumber');
function get(property: string); // this is a required signature today
function get(property: string) {
    // do the getting
}
var result = get('firstName'); // ok
var result2 = get('somethingElse'); // ok
var x = 'hi';
var result3 = get(x); // ok

Alternate reality:

function get(property: 'firstName');
function get(property: 'lastName');
function get(property: 'phoneNumber');
//function get(property: string); // this is no longer required
function get(property: string) {
    // do the getting
}
var result = get('firstName'); // ok
var result2 = get('somethingElse'); // error
var x = 'hi';
var result3 = get(x); // ok

It's a little more verbose to declare than your 'memberof' proposal but it does give you that error checking. It's not clear how often you'd be able to define an API like this that only ever used string literals and never string typed variables though.

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Aug 7, 2014

The problem with the overload on constant signatures is that it requires you to separately define your string constants apart from the actual interface you're working on. A major part of my proposal is that you can use an existing interface to define the valid property names.

In other words, let's say I was writing the TypeScript definition file for Backbone. The definition file today looks something like this:

class Model {
    ...
    get(property: string): any;
    ...
}

It would be much better if I could write this:

class Model<T> {
    ...
    get(property: memberof T): any;
    ...
}

Another example of an API where you would (mostly) use a string literal instead of string typed variables is React: http://facebook.github.io/react/docs/two-way-binding-helpers.html

In particular, the "linkState" function takes an argument which is the name of a property on the state object. Since my state object has an interface it would be ideal if I could ensure at compile-time that the string literal I typed is correct.

jbrantly commented Aug 7, 2014

The problem with the overload on constant signatures is that it requires you to separately define your string constants apart from the actual interface you're working on. A major part of my proposal is that you can use an existing interface to define the valid property names.

In other words, let's say I was writing the TypeScript definition file for Backbone. The definition file today looks something like this:

class Model {
    ...
    get(property: string): any;
    ...
}

It would be much better if I could write this:

class Model<T> {
    ...
    get(property: memberof T): any;
    ...
}

Another example of an API where you would (mostly) use a string literal instead of string typed variables is React: http://facebook.github.io/react/docs/two-way-binding-helpers.html

In particular, the "linkState" function takes an argument which is the name of a property on the state object. Since my state object has an interface it would be ideal if I could ensure at compile-time that the string literal I typed is correct.

@saschanaz

This comment has been minimized.

Show comment
Hide comment
@saschanaz
Contributor

saschanaz commented Aug 17, 2014

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 18, 2014

Member

Yeah, I definitely get how overload on constants doesn't scale super well here. Are there many other frameworks where this would be valuable? There's a high bar for adding new syntax so we'd want to ensure it's solving a reasonably large class of issues. We also need to better understand whether it'd be generally useful to be able to define overloads that can only take string literal values and not computed values/variables.

Member

danquirk commented Aug 18, 2014

Yeah, I definitely get how overload on constants doesn't scale super well here. Are there many other frameworks where this would be valuable? There's a high bar for adding new syntax so we'd want to ensure it's solving a reasonably large class of issues. We also need to better understand whether it'd be generally useful to be able to define overloads that can only take string literal values and not computed values/variables.

@jbrantly
@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Aug 25, 2014

Member

Great examples, very useful to guide our thoughts, thanks.

Looking at your earlier examples again, how do you feel about this returning any?

class Model<T> {
    ...
    get(property: memberof T): any;
    ...
}

You'll get nice checking on the input side now but you're going to have to cast on each getter call or else deal with a lot of unsafe code. To do better than that will require some other non-trivial new features which @RyanCavanaugh and I were just talking about and need to be fleshed out a little further.

Member

danquirk commented Aug 25, 2014

Great examples, very useful to guide our thoughts, thanks.

Looking at your earlier examples again, how do you feel about this returning any?

class Model<T> {
    ...
    get(property: memberof T): any;
    ...
}

You'll get nice checking on the input side now but you're going to have to cast on each getter call or else deal with a lot of unsafe code. To do better than that will require some other non-trivial new features which @RyanCavanaugh and I were just talking about and need to be fleshed out a little further.

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Aug 26, 2014

I thought about the return type but left it out as to not make the overall suggestion too big of a bite.

This was my initial thought but has problems. What if there are multiple arguments of type memberof T, which one does membertypeof T refer to?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

This solves the "which argument am I referring to" problem, but the membertypeof name seems wrong and not a fan of the operator targeting the property name.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

I think this works better.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

Unfortunately not sure that I have a great solution although I believe the last suggestion has decent potential.

jbrantly commented Aug 26, 2014

I thought about the return type but left it out as to not make the overall suggestion too big of a bite.

This was my initial thought but has problems. What if there are multiple arguments of type memberof T, which one does membertypeof T refer to?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

This solves the "which argument am I referring to" problem, but the membertypeof name seems wrong and not a fan of the operator targeting the property name.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

I think this works better.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

Unfortunately not sure that I have a great solution although I believe the last suggestion has decent potential.

@saschanaz

This comment has been minimized.

Show comment
Hide comment
@saschanaz

saschanaz Aug 30, 2014

Contributor

This seems some kind of 'type maps'. Defining this manually will allow potentially more functional string-type enums:

(from my post)

// Has same syntax with interfaces but works only as a type map
typemap Foo {
  madoka: MadokaObject;
  homura: HomuraObject;
  /* ... */
}

class GameCenter {
  // Much shorter function declaration
  createCharacter(charType: Foo is A): A {
  }
}
Contributor

saschanaz commented Aug 30, 2014

This seems some kind of 'type maps'. Defining this manually will allow potentially more functional string-type enums:

(from my post)

// Has same syntax with interfaces but works only as a type map
typemap Foo {
  madoka: MadokaObject;
  homura: HomuraObject;
  /* ... */
}

class GameCenter {
  // Much shorter function declaration
  createCharacter(charType: Foo is A): A {
  }
}
@Taytay

This comment has been minimized.

Show comment
Hide comment
@Taytay

Taytay Dec 12, 2014

Another related proposal would be to add the nameof compile-time operator.
It was just added to C#, and it fixes a subset of this issue in an elegant way.
Description here

At compile time, nameof converts its parameter (if valid), into a string. It makes it much easier to reason about "magic strings" that need to match variable or property names across refactors, prevent spelling mistakes, and other type-safe features.

Using nameof, the initial proposal would be implemented as follows:

interface Person {
    firstName: string
    lastName: string
    phoneNumber: string
}

function get(property: string) {}

get(nameof(Person.firstName)) // compiles
get(nameof(Person.middleName)) // would not compile since middleName is an invalid reference

Taytay commented Dec 12, 2014

Another related proposal would be to add the nameof compile-time operator.
It was just added to C#, and it fixes a subset of this issue in an elegant way.
Description here

At compile time, nameof converts its parameter (if valid), into a string. It makes it much easier to reason about "magic strings" that need to match variable or property names across refactors, prevent spelling mistakes, and other type-safe features.

Using nameof, the initial proposal would be implemented as follows:

interface Person {
    firstName: string
    lastName: string
    phoneNumber: string
}

function get(property: string) {}

get(nameof(Person.firstName)) // compiles
get(nameof(Person.middleName)) // would not compile since middleName is an invalid reference
@ghalle

This comment has been minimized.

Show comment
Hide comment
@ghalle

ghalle Dec 1, 2015

I am currently using Baobab for a project, it is an immutable data tree / cursor library. When using it I have to use something like this (non-working simplified example):

type Foo = {
   a: number;
};
get<T>(property: string): T;

// elsewhere
get<number>('a');

This for one does not check that the property actually exist and also forces me to cast the get to the correct type every time, which can lead to error for example if I change a in Foo to string my get will still be casting it to number.

This suggestion would therefore be really useful to help keep my code as type checked as possible.

I like this proposal from @jbrantly:
get(property: memberof T is A): A;
set(property: memberof T is A, value: A);
My proposal would be something like this:
get(property: A memberof T): A;
set(property: A memberof T, value: A);

The righthand would need to be an object type ({...}, interface, Class) or it would throw an error.
The lefthand side would be optional and only used if you need a reference to the type.

If you try to pass something that doesn't resolve to a constant string to a memberof type it would throw an error.

Or a way to reference a subtype using the parameter name:
get(property: memberof T): T[property];
set(property: memberof T, value: T[property]);

ghalle commented Dec 1, 2015

I am currently using Baobab for a project, it is an immutable data tree / cursor library. When using it I have to use something like this (non-working simplified example):

type Foo = {
   a: number;
};
get<T>(property: string): T;

// elsewhere
get<number>('a');

This for one does not check that the property actually exist and also forces me to cast the get to the correct type every time, which can lead to error for example if I change a in Foo to string my get will still be casting it to number.

This suggestion would therefore be really useful to help keep my code as type checked as possible.

I like this proposal from @jbrantly:
get(property: memberof T is A): A;
set(property: memberof T is A, value: A);
My proposal would be something like this:
get(property: A memberof T): A;
set(property: A memberof T, value: A);

The righthand would need to be an object type ({...}, interface, Class) or it would throw an error.
The lefthand side would be optional and only used if you need a reference to the type.

If you try to pass something that doesn't resolve to a constant string to a memberof type it would throw an error.

Or a way to reference a subtype using the parameter name:
get(property: memberof T): T[property];
set(property: memberof T, value: T[property]);
@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Feb 20, 2016

Contributor

the proposal in #1295 should cover the scenarios outlined in this issue.

Contributor

mhegazy commented Feb 20, 2016

the proposal in #1295 should cover the scenarios outlined in this issue.

@Dominator008

This comment has been minimized.

Show comment
Hide comment
@Dominator008

Dominator008 commented Aug 25, 2016

Looks like this is largely solved by string literal types: https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types ?

@saschanaz

This comment has been minimized.

Show comment
Hide comment
@saschanaz

saschanaz Aug 25, 2016

Contributor

Not really, but I think at least it is a prerequisite to solve this issue. #10425 will do.

Contributor

saschanaz commented Aug 25, 2016

Not really, but I think at least it is a prerequisite to solve this issue. #10425 will do.

@Microsoft Microsoft locked and limited conversation to collaborators Jun 18, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.