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

Suggestion: polymorphic object and tuple member type references through literal types #7730

Closed
malibuzios opened this issue Mar 29, 2016 · 12 comments
Assignees
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@malibuzios
Copy link

(This seems to have some things in common with #6080 and is strongly related to #1295 as it can be used to provide an alternative solution to it but l opened this separately since I did not feel it was appropriate to continue discussing it there and wanted to approach it in a more generalized way)

This would provide improved type safety for cases where an interface property is referenced through a string having a value that's known at compile time:

type MyType = { "ABC": number };
let x: MyType = { ABC: 42 };

const propName = "ABC"; // 'propName' received the literal type "ABC" (not string)
let y = x[propName]; // 'y' received the type MyType["ABC"] which resolved to 'number'

And with support for generic types added, this could work similarly, but resolve to an intermediate type of the form T[S] where T is an object type and S is a string literal type:

function func<T extends object>() {
    let x: T;
    const propName = "ABC"; // 'propName' received the literal type "ABC"
    let y = x[propName]; // 'y' would receive the type T["ABC"]
}

This could combine with readonly function parameters to provide an alternative solution for #1295 (which was actually where the idea was initially proposed):

function getProperty<T extends object, S extends string>(obj: T, readonly propName: S): T[S] {
    return obj[propName];
}

// Here T resolves to { ABC: number }, S resolves to the literal type "ABC", 
// and T[S] resolves to number
let x = getProperty({ABC: 42}, "ABC"); // Type of 'x' is number

// Here T resolves to { ABC: number }, S resolves to the literal type "CBA" 
// and T[S] resolves to any
let x = getProperty({ABC: 42}, "CBA"); // Type of 'x' is any

// Here T resolves to { ABC: number }, S resolves to string
// and T[S] resolves to any
let x = getProperty({ABC: 42}, getRandomString()); // Type of 'x' is any

This may similarly extend to numeric literal types, for tuples:

type MyTupleType = [number, string, boolean];
let x: MyTupleType;
const index = 1; // 'index' received the literal type 1;

let y = x[index]; // 'y' received the type MyTupleType[1] which resolved to 'string'

And work with generic tuples as well:

function getTupleElement<T extends Array<any>, N extends number>(tuple: T, readonly index: N): T[N] {
    return tuple[index];
}

Same can be done with symbol literal types (haven't thought about that much though):

function getSymbolProperty<T extends object, S extends symbol>(obj: T, readonly sym: S): T[S] {
    return obj[sym];
}
@mhegazy
Copy link
Contributor

mhegazy commented Mar 29, 2016

duplicate of #6678

@malibuzios
Copy link
Author

@mhegazy

#6678 does seem to cover some of the tuple aspects (which are only a small part of this), but this one also covers generic tuples as well:

function func<T extends Array<any>>(tuple: T) {
  const index = 1;
  return tuple[index]; // return type inferred as the polymorphic reference type T[1]
}

type MyTuple = [number, string, boolean];
let x: MyTuple = [12, "abcd", true];
let result = func(x); // type of 'result' is the type MyTuple[1] which resolves to string

@mhegazy
Copy link
Contributor

mhegazy commented Mar 29, 2016

this part would be #1295.

we have no way today of getting element type of an array.

@mhegazy mhegazy added the Duplicate An existing issue was already created label Mar 29, 2016
@malibuzios
Copy link
Author

@mhegazy

Please read this comment in #1295. This was initially proposed as an alternative solution the one originally proposed there but is fundamentally different from it. The main difference is that the property specifier here is a type rather than a run-time entity:

The basic idea of a literal type is that it essentially captures the awareness of the compiler of a concrete value that is known to be held by a run-time entity. Since in

const x = "ABC"

x receives the literal type "ABC" (e.g: x: "ABC"), I have reformulated the proposed solution for #1295 to reference a literal type instead of a parameter, e.g. instead of

function getProperty<T extends object>(obj: T, propName: string): T[propName] {
    return obj[propName];
}

I modifiedT[propName] to T[S] where S is a type that extends string:

function getProperty<T extends object, S extends string>(obj: T, readonly propName: S): T[S] {
    return obj[propName];
}

// T is inferred as { abcd: number }, S is inferred as the string literal _type_ "abcd"
// T[S] resolves to the type { abcd: number }["abcd"] which then resolves to number
let x = getProperty({ "abcd": 42 }, "abcd"); // x receives type number

I'm using the fact that in case the parameter propName would receive a value that is known at compile time and the parameter propName is immutable. The type S would be inferred to be a literal string type and can be referenced and eventually resolved in the expression T[S].

I have also generalized this to any position in the code (not only the particular case where it is used return types) and for numeric literals as well (numeric references are not covered by #1295.), and as a pattern that could be used to provide improved type inference in general.

There was a very good reason this was formulated this way and I believe this approach would give a wider benefit to more aspects of the language. I believe this captures the original intention and semantics better (as well as having no mixing of type and run-time entities at type positions) and may also turn out to be easier to implement than the original approach proposed at #1295.

This issue is not a "duplicate" as it describes a pattern that hasn't been mentioned yet (to my best knowledge). It is about discussing and developing a wider understanding of these forms of polymorphic references through literal types. I wanted this to be considered and discussed by the TS team and I did not feel it was appropriate or effective to continue posting about it there (I'm not sure if TS members even follow that discussion).

Edits: significantly expanded and clarified compared to what appeared in the e-mail notification.

@malibuzios
Copy link
Author

Seems like the whole discussion has been diverted to arguing whether this is a duplicate or not, and the actual content ignored. I'm sorry but this feels like a complete waste of time. I'll close it myself.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 30, 2016

I did not intend to wast any one's time. sorry if i did. i believe i understand the proposal, and it seems to be there are two parts, 1. using the values of const initializes to get better type information, which seems to be tracked by #6678, and 2. the ability to talk about type of a property of a type. the proposal here seems to be a slight variation from #1295. as mentioned in #1295 (comment), we are open to accepting PRs for the general proposal, and we would be open to consider your proposed implementation approach as well.
one thing to note, there are no literal types for numbers, or symbols at the moment. so your proposal only works for string literal types.

@malibuzios
Copy link
Author

@mhegazy

That's OK, I did not mean you were wasting my time, more like "it turned out it wasn't that much of a great idea to open a new issue and wasn't positively received, never mind". Sometimes in online conversations there could be some misunderstandings, so it could be one.

Anyway, the modification I proposed for #1295 could be seen as "minor" in the sense that it is still the same syntax and the general approach in relation to the problem presented is very similar (though it does require type argument inference to be truly effective), but is different (perhaps not "fundamentally" different - that was probably an exaggeration) in the sense that the two cannot work together - either run-time entities are used as specifiers (e.g. T[propName]) or types (e.g. T[S]), having them both would create problems with name collisions etc. and would have somewhat ambiguous semantics.

The features this approach relies on weren't available or even proposed when #1295 was conceived. Things like string literal types and how they are applied to immutable run-time entities and the possibility of read-only function parameters (though I'm not sure if that has been decided on at this point).

There are also some interesting extensions that can be done with this, seemingly without significant effort as it relies on conventional inference mechanisms of the language. For example, as a complement to #6080 (or maybe as an application for #7722) it could support more complex scenarios like:

function getAnyOneOfTwoStrings(): "a" | "b" {
    if (Math.random() > 0.5)
      return "a";
    else
      return "b";
}

function f<T extends object, S extends string>(obj: T, readonly str: S) {
  return obj[str];
}

let MyType = { a: number, b: string, c: boolean }
let x: MyType;

// T resolves to MyType, S resolves to "a" | "b"
// MyType["a" | "b"] resolves to number | string
let result = f(x, getAnyOneOfTwoStrings()); // type of result is number | string

This may be very difficult or even impossible to implement with the original proposal, but may come essentially "for free" with this slight modification.

@mhegazy mhegazy reopened this Mar 30, 2016
@mhegazy mhegazy added Suggestion An idea for TypeScript and removed Duplicate An existing issue was already created labels Mar 30, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Mar 30, 2016

As mentioned in #1295 (comment) we would love to look at an implementation proposal for this feature, regardless of the approach. i think both approach has merits, and looking at an implementation proposal can illuminate additional information about the correct way to go.
The core team has no plans to tackle this feature in the short term, so PRs are welcomed.

@malibuzios
Copy link
Author

I don't think the examples I gave so far were convincing enough to truly demonstrate the strength of this approach.

The previous example could still somehow (though not very elegantly) use type inference to "pull out" the possible list of literals even with the previous approach:

function func<T>(obj: T, propName: string): T[propName] {
    ...
}

let x: "a" | "b;

func<T>({a: 123, b: "ABC"}, x);

I mean, even if this looks like a real 'stretch' or somewhat semantically inaccurate.

However there are some cases where there is no possible way to get any type information, as there isn't even a particular parameter to reference!

function func<T>(obj: T, getPropName: () => string): T[???]; // What would T reference here?

But when using the type as a specifier, this isn't really a problem, the existing machinery of type argument inference would easily infer it:

// No problem here:
function func<T extends object, S extends string>(obj: T, getPropName: () => S): T[S]

// T resolves to {a: number, b: string}, S resolves to "a" | "b"
// return type is {a: number, b: string}["a" | "b"] which resolves to  number | string
func({a: 123, b: "ABC"}, () => Math.random() > 0.5 ? "a" : "b");

This can be extended to arbitrarily complex expressions, as complex as type argument inference could support:

// No problem here as well:
function func<T extends object, S extends string>(obj: T, complexObject: { num: number, getPropName: () => S}): T[S]

// T resolves to {a: number, b: string}, S resolves to "a" | "b"
// return type is {a: number, b: string}["a" | "b"] which resolves to  number | string
func({a: 123, b: "ABC"}, { num: 123, getPropName: () => Math.random() > 0.5 ? "a" : "b"});

@DanielRosenwasser
Copy link
Member

Is this related to #6080?

@malibuzios
Copy link
Author

@DanielRosenwasser

Yes, I have mentioned it may provide a solution to it but it goes much further than that. It also includes intermediate polymorphic expressions like T["a" | "b"], or even T[S] where both T and S are generic parameters, and T must extend object and S must extend string. Also it describes similar functionality of for tuples, i.e. T[N] where T must extend Array<any> and N must extend number.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 2, 2016

The request in the OP should be handled by the new keyof and T[K] operators introduced by #11929

@mhegazy mhegazy closed this as completed Nov 2, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants