microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.69k stars 12.45k forks source link

add object initializers #8545

Closed zpdDG4gta8XKpMCd closed 2 years ago

zpdDG4gta8XKpMCd commented 8 years ago

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

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 commented 8 years ago

type assertion?

function extend(doThis: DoThis): DoBoth {
    return doThis as DoBoth;
}
zpdDG4gta8XKpMCd commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

mhegazy commented 8 years ago

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 commented 8 years ago

now as i read closer it looks like a solution

mhegazy commented 8 years ago

Object spread and rest is tracked by https://github.com/Microsoft/TypeScript/issues/2103

zpdDG4gta8XKpMCd commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

zpdDG4gta8XKpMCd commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

so is this a different proposal for https://github.com/Microsoft/TypeScript/issues/8353?

zpdDG4gta8XKpMCd commented 8 years ago

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" notifications@github.com wrote:

so is this a different proposal for #8353 https://github.com/Microsoft/TypeScript/issues/8353?

— You are receiving this because you modified the open/close state. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/8545#issuecomment-218917157

mhegazy commented 8 years ago

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.

afnpires commented 7 years ago

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

zpdDG4gta8XKpMCd commented 7 years ago

@afnpires SO question is about different matters

RyanCavanaugh commented 6 years ago

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

zpdDG4gta8XKpMCd commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

zpdDG4gta8XKpMCd commented 6 years ago

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 commented 6 years ago

Hey @aleksey-bykov, do you have any suggestion for that syntax?

zpdDG4gta8XKpMCd commented 6 years ago

@lilezek yes please:

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

@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 commented 6 years ago

did you try this:

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

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

zpdDG4gta8XKpMCd commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

please do

lilezek commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

~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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 2 years ago

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