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

add object initializers #8545

Closed
zpdDG4gta8XKpMCd opened this issue May 10, 2016 · 48 comments
Closed

add object initializers #8545

zpdDG4gta8XKpMCd opened this issue May 10, 2016 · 48 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

Currently there are 2 ways to enforce implementing an interface over an object:

  • creating a object literal in an interface type context
  • constructing an instance of a class that implements the said interface

In both cases we deal with a newly created object, however there are also situations when an already created object needs to be extended to comply to some more elaborate interface.

A good example are mixins and scenarios alike:

interface DoThis {
   x: any;
   y: any;
}
interface DoThat {
   z: any
}
interface DoBoth implements DoThis, DoThat {
}

function extend(doThis: DoThis): DoBoth {
    // need to make sure that all properties of DoThat are set
    // wish could do just
    //       doThis.z = undefined;
    //       return doThis;
    // have to do
    return {
        x: doThis.x,
        y: doThis.y
        z: undefined
    };
}

let doThis : DoThis = { x: undefined, y: undefined };
let doBoth = extend(doThis);

I am not aware how to make the extend function type safe, other than copying each field from doThis to the resulting object. Which might or might not be a solution (functions are harder to extend this way). If only TypeScript had a way to enforce an interface on the given object it would be very helpful.

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2016

type assertion?

function extend(doThis: DoThis): DoBoth {
    return doThis as DoBoth;
}

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented May 10, 2016

no, quite opposite

first off it will break if you do as you say

secondly, type assertions is a way for taking responsibility off the compiler, whereas I am looking for the opposite

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2016

the only thing i can think of other than type assertion would be to spread in the other object.

function extend(doThis: DoThis): DoBoth {
    return {
        z: undefined,
        ...doThis
    };
}

@zpdDG4gta8XKpMCd
Copy link
Author

nice but what about primitives and functions in the doThis position?

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2016

Spread should be sugar for object.assign, so no non-enumrable and only own properties. Not sure if this is what you had in mind.

@zpdDG4gta8XKpMCd
Copy link
Author

now as i read closer it looks like a solution

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2016

Object spread and rest is tracked by #2103

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented May 12, 2016

gonna need to reopen it

one more useful scenario

function initializeAsDoThis<T unlike null | undefined>(obj: T => DoThis) { // <-- possible syntax for object that needs to be initialized?
    obj.x = undefined;
    obj.y = undefined;
}

class C implements DoThis  {
    constructor() {
        initializeAsDoThis(this);
    }
}

@zpdDG4gta8XKpMCd
Copy link
Author

Spread should be sugar for object.assign, so no non-enumrable and only own properties. Not sure if this is what you had in mind.

spread operator is only useful at creating new objects, but for the cases where we deal with an existing object we can't use it

@mhegazy
Copy link
Contributor

mhegazy commented May 12, 2016

i am not sure i understand what this sample is meant to do any why it can not be expressed using existing constructs.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented May 12, 2016

problem:
say we have 20 interfaces each with 10 properties
it's easy to manifest that a our class is going to implement all 20 of them

  1. it's much harder to come up with a list of 20x10=200 declared/initialized properties of this class that currently need to be spelled inside the class (there is no way to take property declaration/initilization outside the class)
  2. we cannot utilize inheritance for this because it's very unlikely that our class hierarchy has 20 ancestors classes that each implement one of those 20 interfaces
  3. we cannot delegate property initialization to a sub routine (just like we can do in vanila JavaScript)

workaround: none, we have to declare and initialize 200 properties by hand

solution: with initilizers we could have 20 functions which we would call from the constructor, each initilizer would add 10 initialized properties of a corresponding interface, we need a way for TypeScript to acknowledge that these properties are there and the class should be considered fully initialized

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented May 12, 2016

definition:

object initializer - is a function/method with a parameter that has 2 types: in and out, at the call site the function expects a value of the in-type to be used as an arugument, after the function is called argument should be considered being of the out-type

function initialize<a>(
   value: a /* <-- in type */ => a & { x: number } /* <-- out type */
): void { // <-- hypothetical syntax
   value.x = 100;
}
let value = {};
value.x; // <-- should not typecheck
initialize(value);
value.x; // <-- should typecheck

@mhegazy
Copy link
Contributor

mhegazy commented May 12, 2016

so is this a different proposal for #8353?

@zpdDG4gta8XKpMCd
Copy link
Author

I think it is different. Main difference is that rather than trying to
track all possible execution branches that might reassign a variable (which
makes it a very hard task) I am proposing a simpler task of enforcing a
type change within the immediate scope of a dedicated function without
accounting for anything might happen in a subroutine.
On May 12, 2016 7:47 PM, "Mohamed Hegazy" [email protected] wrote:

so is this a different proposal for #8353
#8353?


You are receiving this because you modified the open/close state.
Reply to this email directly or view it on GitHub
#8545 (comment)

@mhegazy
Copy link
Contributor

mhegazy commented May 13, 2016

we have talked about something similar proposals before; the main reason for aversion is complexity. once you mix in generics, these declarations become harder to read and understand.

@mhegazy mhegazy added the Suggestion An idea for TypeScript label May 13, 2016
@afnpires
Copy link

You can achive something similar with Mapped Types and Partials, according to this StackOverflow answer.

@zpdDG4gta8XKpMCd
Copy link
Author

@afnpires SO question is about different matters

@RyanCavanaugh
Copy link
Member

@Aleksey-Bykov can you clarify which aspects (if any) can't be accomplished today?

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Aug 13, 2018
@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 14, 2018

yes please

  1. you have an object a of type A
  2. a gets passed to a foo function
  3. after function is called the value a must be of type B

example:

type A = {}
const a: A = {};
type B = { x: number; y: number };
function foo(a: {}): void { // <-- need syntax to express the effect
   a.x = 0;
   a.y = 0;
}
const b: B = a; // <-- works because `a` is of type `B` now after `foo` is called on it

@zpdDG4gta8XKpMCd
Copy link
Author

the only way to accomplish it today is to use so-called "mixins" via classes (which is as ugly as my life) or hacking a to b using casting, whereas in vanila javascript code this pattern is natural and very popular (which is one of your goals - support idiomatic javascript), so here i am

@RyanCavanaugh
Copy link
Member

Looking at that example I see either #10421 or #22865

@zpdDG4gta8XKpMCd
Copy link
Author

i looked at them the second one is what i am taking about but i dont like the syntax, and besides this issue is 12000 issues ahead of that one

@lilezek
Copy link

lilezek commented Aug 28, 2018

Hey @Aleksey-Bykov, do you have any suggestion for that syntax?

@zpdDG4gta8XKpMCd
Copy link
Author

@lilezek yes please:

function initializeXY<T extends {}>(obj: T => T & {x: number; y: number;}): void {
   obj.x = 0;
   obj.y = 0;
}

@lilezek
Copy link

lilezek commented Aug 30, 2018

@Aleksey-Bykov That's good for extending, but I think it can't be used for reducing an object:

const obj = {x: 15, y: 13};
delete obj.x; // Or a function that does the delete for you.

After the delete sentence you couldn't represent the type of the obj without a cast.

Edit: I think I didn't understand your syntax but isn't that the syntax for a function rather than the syntax for an object?

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

did you try this:

function getridofY<T extends { y: unknown }>(obj: T => T & { y: never; }): void {
   delete obj.y;
}

@lilezek
Copy link

lilezek commented Aug 30, 2018

Sorry, as I wrote in the edit in my comment: isn't that the actual syntax for functions?

@zpdDG4gta8XKpMCd
Copy link
Author

functions require a list of parameters which has to be enclosed into parenthesis (value: number) => number or () => number whereas here we have T => T (observe no parameter list)

@lilezek
Copy link

lilezek commented Aug 30, 2018

Yes, that's correct, but these are so similar I think that it would be easy to be confused about that. Anyway, if you want I can put your suggestion in my issue description as an additional option.

@zpdDG4gta8XKpMCd
Copy link
Author

please do

@lilezek
Copy link

lilezek commented Aug 30, 2018

Before I do I need first some description about how it would work with that syntax. For instance, if you want to convert something from string to be a number:

function normalizePhoneNumber<T extends {phone: string}>(obj: T => T & { phone: number }): void {
   obj.phone = parseInt(obj.phone, 10);
}

What type is obj inside the function? If it is {phone: string} & { phone: number } then that line will not pass TypeScript checks.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

that's a very good question,

  1. at the beginning of the function obj is of type T extends { phone: string }
  2. inside the property can be assign from number to string back and forth as many times as needed (effectively being string | number)
  3. we use flow analysis to make sure that the property is of type string prior to any return statement or the end of an execution branch (which imply a return statement of void)

@lilezek
Copy link

lilezek commented Aug 30, 2018

In that case wouldn't be just more simple to use a syntax similar to the one I proposed? Using two different identifiers, with two different types that will be compiled in JavaScript as the same one?

I don't know this much about TypeScript compiler, but I think that 2nd and 3rd point are harder to achieve than having two separate identifiers.

@zpdDG4gta8XKpMCd
Copy link
Author

i agree, it might be easier to achieve by using 2 different identifiers, question of the balance of the price tag of this feature vs. happiness of the developers who tend to like typing less

@zpdDG4gta8XKpMCd
Copy link
Author

on the second thought it might be more confusing to have a virtual identifier that has no meaning in the real code

unless typescript does code rewriting by replacing x2 by x which to my knowledge goes against its goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals

5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

@zpdDG4gta8XKpMCd
Copy link
Author

also it can be a burden for developers to pick a name for that extra identifier, in my experience picking a meaningful name is 20% of my day job

@lilezek
Copy link

lilezek commented Aug 30, 2018

I think your suggestion and mine are not comparable in size. While you use generics and that adds < T extends >, T => T &, and two types (the type before and the type after), mine adds then and two identifiers (with their types). So basically any of them can be bigger or smaller depending on the size of the types and identifiers.

On the other hand, I agree that having a second, virtual identifier, can be confusing and it might be even against TypeScript goals. I'll try to think about another suggestion without a virtual identifier, but I wouldn't go either to use generics + a syntax that is pretty similar to arrow functions, plus having a flow analyser which could be hard to implement and that I that is still undefined how it should work.

About choosing a name, you can choose an arbitrary style for the name of the second virtual identifier, such as x then xAfter or such.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

generics are necessary because it's the way to generalize and express uncertainties and about your types, i can't see how you can go without them, you would have to reinvent them at some point or greately limit the scope of applicability of this feature

flow analysis is already implemented, we just need to make use of it:

declare var a: number | string;
a = 'hey';
const text: string = a;

arrow syntax is already familiar to anyone who used callbacks

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

since the syntax concerns the types (not the JS expressions) it can be literally anything: =>, ->, ::, >>, <!>, becomes, etc

what's important is that syntax needs to be bound to the parameter in place

@lilezek
Copy link

lilezek commented Aug 30, 2018

While I agree with everything you said, I don't see the uncertainty of types to need the use of generics. For instance, using the syntax I proposed (I use this one because I don't have yet a better option), the types here are exact and not uncertain:

interface Before {
  address: string;
}

interface After {
  addr: string;
}

function map(userb: Before then usera: After) {
   usera.addr = userb.address;
   delete userb.address;
}

You know exactly what must be the type before and you know exactly what will it be after. And this could be mixed with generics if any of these types (the type before and the type after) are unkown:

function inlineMap<T,U>(arrayB: T[] then arrayA: U[], mappingFunc: (T) => U) {
  for (let i = 0; i < arrayB.length; i++) {
    arrayA[i] = mappingFunc(arrayB[i]);
  }
}

@zpdDG4gta8XKpMCd
Copy link
Author

problem is that in my use cases i don't know much if anything at all about what objects will be passed into my initializer function

think of the mixin pattern, i want to turn my very own object into something that has x and y properties and able to be manipulated by changing them

Before and After in your example are cute but what if i need the same addr / address behavior applied to something else?

my point is that Before should be rather a generic, because you have no idea upfront what it will really be

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

your last example is not going to work without generics because T and U as declared (bare generics) have nothing to do with having { addr: } or any other props

so you are proposing to pass a callback for mutating bare T and U each time? in the bottom of my heart i like it a lot (it resonates with the category theory where you morph abstract objects not knowing anything about their nature), but this is not how generics are used in TS typically

and by this i mean that in TypeScript rather than passing 10 callbacks along with your generics, you instead simply require not a bare generic but something that has x, y, and z of type, say, number so that you can work off that, which leads us to <T extends { x: number, y: number, x: number }>

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 30, 2018

besides say you have

interface Circle { x: number; y: number; r: number; }
interface Line { x1: number; x2: number, y1: number, y2: number; }
interface Box { ... }
interface Triangle { ... }
interface Star { ... }
interface NGon { ... }

now i want to add color to all/any of them, according to you i need a special function for each single type of object

now i want to add label to all/any of them, again i need a special function for each single type of object

it's just silly

@lilezek
Copy link

lilezek commented Aug 31, 2018

You don't need 10 callbacks. I think I'm using the generics as they are being used in the definitions of TypeScript for arrays:

interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}

And with the proposed syntax example, if you want to add colour to all of them you could just do:

function addColour<T extends {}>(primitive: T then primitiveWithColour: T & {colour: string}, colour: string) {
  primitiveWithColour.colour = colour;
}

Generics are totally compatible with that, but they are just not mandatory.

@zpdDG4gta8XKpMCd
Copy link
Author

this feature has to be build around generics, generics are the main use case, the main use case calls for prime time support from the language, using non-generics would be a special case

10 callbacks are necessary if you don't want to deal with extends, i can give you an example but too lazy to write it, if extends is not a problem then 10 callbacks are not required

@lilezek
Copy link

lilezek commented Sep 3, 2018

Why it has to be built around generics? They can be used, but I don't see the need for mandatory usage of generics if you know exactly the type before and the type after the change.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Sep 3, 2018

i didn't say generics are mandatory, all i said that generics are the main use case while specific types are a special case, and the reason i brought it up is that the syntax should rather be favoring the main case, not the special case

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision and removed Needs More Info The issue still hasn't been fully clarified labels Dec 16, 2021
@RyanCavanaugh
Copy link
Member

This doesn't come up often enough to justify the investment necessary to create this behavior

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants