How to extend anonymous type in Typescript
Asked Answered
R

3

5

When chaining functionality in typescript with anonymous types for example like this:

  let array = [{ seed: 2 }, { seed: 3 }];
  array
    .map(i => ({ seed: i.seed, square: i.seed * i.seed }))
    .forEach(i => console.log(`square for ${i.seed} is ${i.square}`));

I need to define new anonymous type for map function. If I would have multiple steps all producing new properties, I'd end up writing lots of definition code to get all properties carried over. I could use $.extend (or Object.assign), but that way I'll lose intellisense and strong typing.

  array
    .map(i => $.extend(i, { square: i.seed * i.seed }))
    .forEach(i => console.log(`square for ${i.seed} is ${i.square}`));

How can I extend anonymous object without defining all properties again while keeping strong typing?

Rickierickman answered 12/4, 2016 at 11:32 Comment(0)
R
5

I have finally found a solution. This can be achieved by Intersection Types. Those can be used with anonymous types and classes. In the example below, extend function will copy properties from each of objects and return an object of Intersection Type. This will reduce considerable amount of type definition code without losing intellisense and strong typing.

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

let array = [{ seed: 2 }, { seed: 3 }];

array
    .map(i => extend(i, { square: i.seed * i.seed }))
    .map(i => extend(i, { cube: i.square * i.seed }))
    .forEach(i => console.log(`square for ${i.seed} is ${i.square} and cube is ${i.cube}`));

Same in Playground

This is implemented f.e. in core-js and its type definitions return Intersection Type:

assign<T, U>(target: T, source: U): T & U;
Rickierickman answered 6/10, 2016 at 12:20 Comment(1)
Could you please share an example how to import and use core-js implementation? This will help folks like me who don't want to introduce own definition of extend() function to their project. Thanks!Siren
V
3

To add an easier to understand answer about how one can extend a type with an anonymous type by using intersection types, I made this example that illustrates it:

interface Animal {
  name: string
}

// With named types:

interface Bear extends Animal {
  honey: boolean
}

function giveHoney(bear: Bear) {
  bear.honey = true;
}

const myBear: Bear = { name: 'Bob', honey: false };
giveHoney(myBear);

// With anonymous types (no need to explicitly define the Bear type):

function giveHoney(bear: Animal & { honey: boolean }) {
  bear.honey = true;
}

const myBear = { name: 'Bob', honey: false };
giveHoney(myBear);
Venireman answered 22/4, 2022 at 7:24 Comment(0)
S
2

How about:

interface A {
    seed: number;
}

interface B extends A {
    square: number;
}

let array: A[] = [{ seed: 2 }, { seed: 3 }];
array
    .map<B>(a => { 
        return { seed: a.seed, square: a.seed * a.seed } 
    })
    .forEach(b => console.log("square for ${b.seed} is ${b.square}"));

or (if you want to keep things anonymous):

let array = [{ seed: 2 }, { seed: 3 }];
array
    .map<{seed: number, square: number}>(a => {
        return { seed: a.seed, square: a.seed * a.seed }
    })
    .forEach(b => console.log("square for ${b.seed} is ${b.square}"));

(use it in playground)

Sorn answered 12/4, 2016 at 11:48 Comment(8)
You are still defining seed again in return value. How about, if there are 5 parameters and map will add 1 parameter. What a mess that would be.Rickierickman
@JukkaVaris so use the first solution with the interfaces. There's no way (that I'm aware of) to extend it otherwiseSorn
I'm afraid of that. My actual use case is with RxJS and there chains can be pretty long. Interface graph would grow out of hand easily. I use anonymous types to decrease amount of code I need to write and I would like to see that compiler could infer types from usage.Rickierickman
@JukkaVaris The whole point of typescript is to declare types, if you don't want to go through the hustle of doing that then just use javascript. Yes, it's way more verbose to define all of your types with interfaces but then you get the benefits.Sorn
If you are using latest typescript, 1.8 at the moment, you don't need to explicitly declare all types. There is concept called contextual typing, which will infer types for function parameters. Therefore your second example is having lots of overhead compared to my original example.Rickierickman
@JukkaVaris Well then, what's the problem? if your original example is working well for you then what exactly are you asking?Sorn
My example is working by means of strong typing, but it introduces more code than I would like to write. It is simplified to have only 2 properties, but in real world there are usually more parameters. [{a:1, b:2, c:3, d:4}].map(data => ({a:data.a, b: data.b, c: data.c, d: data.d, result: calculate(data) }).map(data => ({a: data.a, b: data.b, c: data.c: d: data.d, result: data.result, checksum: calculateChecksum(data)}). Can you see the problem here? It would be easier to just somehow extend anonymous type. How many changes there would be to get one more property to be added to seed data?Rickierickman
Well, I think you're just thinking about this the wrong way. Why do you want each step to extend the previous one? why not just encapsulating it? That will solve your "problem" and in my opinion it's a better and more robust design.Sorn

© 2022 - 2024 — McMap. All rights reserved.