Create array with an additional property - javascript

I'm using an existing 3rd party API over which I have no control and which requires me to pass an array with an additional property. Something like the following:
type SomeArgument = string[] & { foo: string };
doSomething (argument: SomeArgument);
Now I find it quite clumsy and verbose, to create SomeArgument while keeping the compiler satisfied.
This one works, but no type safety at all:
const customArray: any = ['baz'];
customArray.foo = 'bar';
doSomething(customArray);
Another option which feels cleaner, but is quite verbose (I will need different subclasses with different properties) is to subclass Array:
class SomeArgumentImpl extends Array<string> {
constructor (public foo: string, content?: Array<string>) {
super(...content);
}
}
doSomething(new SomeArgumentImpl('bar', ['baz']));
Is there any better, one-liner-style way? I was hoping for something along doSomething({ ...['baz'], foo: 'bar' }); (this one does not work, obviously).

Suppose this is the function you want to call:
function doSomething (argument: string[] & { foo: string }) {
argument.push("a");
argument.foo = "4";
console.log(argument);
}
Then you can call it like this:
// Works
doSomething(Object.assign(["a", "b"], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: 4 }));

Related

TypeScript nested object interface with known subset of keys

I have a JavaScript object created like this.
const meta = {
baz: null
}
const response = {
foo: 1,
bar: {
foo: 2,
buzz: {
foo: 3,
...meta
},
...meta
},
...meta,
qux: ‘qux’
}
The response object is a plain object that can contain any key. We know the type of keys foo, baz, and qux. We don’t know the other key names but when they do exist, they are objects with a known interface. In fact, the response object is an instance of this interface.
How do I create a TypeScript interface expressing this logic? I want the compiler to allow defining objects like this:
const foo: MyInterface = {
foo: 1,
dynamicKey: {
baz: null
}
}
Index signatures might work if you are okay with this limitation.
This worked for my use case. The trick was to expand the index signature interface with all values. This has a limitation as was pointed out, but it's a much better situation than not having any type checking or autocompletion. My interface now looks like this.
interface MyInterface extends KnownInterface {
[key: string]: MyInterface | KnownInterface[keyof KnownInterface];
}

FlowJS: How to use union types and boolean literals

Flow questions seem to garner few answers, but here goes:
type Base = {
foo: string,
bar: string
}
type Derived1 = Base & {
conditional: false
}
type Derived2 = Base & {
conditional: true,
baz: string
}
type One = {
foo: string,
bar: string,
conditional: boolean
}
type Two = One & {
baz: string
}
type Return1 = Derived1 | Derived2 // fails
type Return2 = One | Two // works, but not desired
function test(conditional: boolean): Return1 {
return {
foo: "foo",
bar: "bar",
conditional,
...conditional ? {baz: "baz"} : {}
}
}
It is preferable for the return value of test to be one of the Derived* types (Return1 rather than Return2), where the conditional property is a boolean literal.
The intention is for flow to understand that if conditional is true, then the object test returns must contain baz and vice versa.
Is this not possible?
Flow isn't quite smart enough to figure it out for you. You have to do something like:
function test(conditional: boolean): Return1 {
const base = {
foo: "foo",
bar: "bar",
}
if (!conditional) {
return Object.assign({}, base, { conditional });
} else {
const result: Derived2 = Object.assign({}, base, { conditional }, {baz: "baz"});
return result;
}
}
More info here: https://flow.org/blog/2016/07/01/New-Unions-Intersections/

Named default ES6 parameter without destructuration

I'm trying to default options in ES7 using babel. Here is what I can do:
class Foo {
constructor({key='value', foo='bar', answer=42}) {
this.key = key;
this.foo = foo;
this.number = number;
}
}
This might work for this example, but I would like to know how can I assign for very large config objects; here is an example of what I wanna do:
class Foo {
constructor(opts = {key='value', foo='bar', answer=42}) {
this.opts = opts;
}
}
However this does not compile. I tried to do it like this:
class Foo {
constructor(opts = {key:'value', foo:'bar', answer:42}) {
this.opts = opts;
}
}
But then it replaces the whole object, like this:
let foo = new Foo({key: 'foobar'});
console.log(foo.opts);
// {key: 'foobar'} is what is displayed
// When I want {key: 'foobar', foo:'bar', answer:42}
I don't think you can do this with ES6 optional parameters (object as a parameter with optional keys), because when you call the constructor, it's a new object with a new reference. That's because it's being replaced.
But, as a suggestion, if you want to handle a large options object, one common approach is store somewhere a default options Object and merge the object with the one passed when you instantiate it.
Something like that:
class Foo {
constructor(opts) {
this.opts = Object.assign({}, Foo.defaultOptions, opts)
console.log(this.opts)
}
}
Foo.defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
let foo = new Foo({key: 'another value'})
//{ key: 'another value', foo: 'bar', answer: 42 }
You can merge with Object.assign (be aware that it does not perform deep merging - nested objects are replaced).
Or, if you want to declare your default options Object as a class variable (not at the end, after class declaration, or inside constructor), as you're using babel, you can use this plugin and do this:
class Foo {
defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
constructor(opts) {
this.opts = Object.assign({}, this.defaultOptions, opts)
console.log(this.opts)
}
}
It's more readable.
It is
class Foo {
constructor({key='value', foo='bar', answer=42} = {}) {
...
}
}
It is ES6 destructuring feature, not specific to ECMAScript 7 (ECMAScript Next) proposals.
Without destructuring it is usually done with object cloning/merging, Object.assign comes to help:
class Foo {
constructor(opts = {}) {
this.opts = Object.assign({
key: 'value',
foo: 'bar',
answer: 42
}, opts);
}
}

Typescript. How to get object's property name from its value?

I'm writing a typescript class for using with immutable map
class NavigableObject<T> {
constructor(private obj: T, private path: string[] = []) { }
To<R>(p: (x: T) => R): NavigableObject<R> {
return new NavigableObject<R>(p(this.obj),
this.path.concat(this.getPropName(p(this.obj))));
}
getPath() {
return this.path;
}
private getPropName(value) {
for (var item in this.obj) {
if (this.obj[item] === value) {
return item;
}
}
}
}
let test = {
a: {
a1: 1,
a2: 1
},
b: {
b1: 1,
b2: 2
}
}
let navigableTest = new NavigableObject(test);
navigableTest.To(m => m.b).To(m => m.b2).getPath(); // = ["b", "b2"]
navigableTest.To(m => m.a).To(m => m.a2).getPath(); // = ["a", "a1"] <-- I selected a2, tho
There is a problem with getPropName method. When obj has two properties with same value, only the first property will be matched.
Does anyone know how to work around this?
You could use this way of getting the property name:
class NavigableObject<T> {
constructor(private obj: T, private path: string[] = []) { }
To<R>(p: (x: T) => R): NavigableObject<R> {
return new NavigableObject<R>(p(this.obj),
this.path.concat(this.getPropName(p)));
}
getPath() {
return this.path;
}
private static propertyRegEx = /\.([^\.;]+);?\s*\}$/;
private getPropName(propertyFunction: Function) {
return NavigableObject.propertyRegEx.exec(propertyFunction.toString())[1];
}
}
let test = {
a: {
a1: 1,
a2: 1
},
b: {
b1: 1,
b2: 2
}
}
let navigableTest = new NavigableObject(test);
navigableTest.To(m => m.b).To(m => m.b2).getPath(); // = ["b", "b2"]
navigableTest.To(m => m.a).To(m => m.a2).getPath(); // = ["a", "a2"]
The future is here. As per Tim Perry's link, TypeScript has now added keyof, which is a great feature for getting the available properties of a class.
Usage, as per the TypeScript documentation:
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string
There's nothing easy you can do here with your current querying approach. It's ambiguous which property you're selecting, so there's going to be no easy way to get the right path. It's not that your code is 'wrong' now as such, it's just that there's two possible correct answers, and it's picked one arbitrarily.
You can change the rules on which of the possible keys it picks, but there are no sensible rules that will reliably get you the right single answer. Alternatively you could return all the possible answers, and have an ambiguous path, but it doesn't seem like that does what you're looking for.
There might be one option by doing crazy things like parsing the function provided with Esprima or even regular expressions to work out which properties are being grabbed, but it's generally going to be a bad idea. That's likely to be complex and fragile, unreliable unless you can guarantee the exact shape of the code you'll be provided in To(), and run pretty slowly too.
If you do want to be able to select properties like this and know the path used for certain, you'll have to give your To function the key of the property, not just a function to get the value it holds. It's a less elegant API, but it's the only sensible way you're going to get the behaviour you're looking for:
class NavigableObject<T> {
constructor(private obj: T, private path: string[] = []) { }
To<R>(p: string): NavigableObject<R> {
return new NavigableObject<R>(this.obj[p],
this.path.concat(p));
}
getPath() {
return this.path;
}
}
let test = {
a: {
a1: 1,
a2: 1
},
b: {
b1: 1,
b2: 2
}
}
let navigableTest = new NavigableObject(test);
navigableTest.To('b').To('b2').getPath();
navigableTest.To('a').To('a2').getPath();
In future it might be possible to do this with type safety in TypeScript, but not right now. This PR is looking into exactly these issues, but it's still under some discussion yet, so there'll be a while until it's implemented. Note that it's still proposing the string-based approach from the example above, it's just the type system will check that string constants are valid property names for the types used.
Don't terminate the for on the first value found, and return an array of names with matching values. I leave to you the propblem of how to handle multiple names.
private getPropName (value) {
var items = [];
for (var item in this.obj) {
if (this.obj[item] === value)
items.push (item);
return items;
}

Has been called with object assertion

Function I'm spying on, receives object as an argument. I need to assert that the function been called with certain properties of the object.
e.g: my SUT has:
function kaboom() {
fn({
foo: 'foo',
bar: 'bar',
zap: function() { ... },
dap: true
});
}
and in my test I can do this:
fnStub = sinon.stub();
kaboom();
expect(fnStub).to.have.been.called;
and that works (it's good to know that fn's been called). Now I need to make sure that the right object has been passed into the function. I care about only foo and bar properties, i.e. I have to set match for specific properties of the argument. How?
upd: sinon.match() seems to work for simple objects. Let's raise the bar, shall we?
What if I want to include zap function into assertion? How do I make that work?
Assuming you're using sinon-chai, you can use calledWith along with sinon.match to achieve this
expect(fnStub).to.have.been.calledWith(sinon.match({
foo: 'foo',
bar: 'bar'
}));
To achieve called.with partial object check, you can also use chai-spies-augment package (https://www.npmjs.com/package/chai-spies-augment) :
Usage (please, notice to .default after importing):
const chaiSpies = require('chai-spies');
const chaiSpiesAugment = require('chai-spies-augment').default;
chai.use(chaiSpies);
chai.use(chaiSpiesAugment);
usage :
const myStub = { myFunc: () => true };
const spy1 = chai.spy.on(myStub, 'myFunc');
myStub.myFunc({ a: 1, b: 2 });
expect(spy1).to.have.been.called.with.objectContaining({ a: 1 });

Categories

Resources