Monthly Archives: September 2019

Apollo GraphQL client

Based upon our previous implementation(s) of a server it’s now time to write some client code.

  • yarn add apollo-client
  • yarn add apollo-cache-inmemory
  • yarn add apollo-link-http
  • yarn add graphql-tag
  • yarn add isomorphic-fetch
  • yarn add -D @types/isomorphic-fetch

Now let’s create a simple script entry (this extends the previous post’s scripts section)

"scripts": {
  "build": "tsc",
  "client": "node client.js"
}

Now let’s create the file client.ts which should look like this

import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';
import fetch from 'isomorphic-fetch';

const cache = new InMemoryCache();
const link = new HttpLink({
  uri: 'http://localhost:4000/',
  fetch: fetch
})

const client = new ApolloClient({
  cache,
  link,
});

client.query({
  query: gql`
    {
    users {
      firstName
      lastName
    }
  }`
  })
  .then((response: any) => console.log(response.data.users));

The response.data.users should now output the array of User objects.

Here’s an example of a mutation

client.mutate({
  mutation: gql`
    mutation {
      create(user: {
        firstName: "Miner",
        lastName: "FortyNiner"
      }) {
        firstName
        lastName
      }
    }
  `
}).then((response: any) => console.log(response.data.create));

Note: I had lots of issues around the fetch code, with errors such as Invariant Violation:
fetch is not found globally and no fetcher passed, to fix pass a fetch for your environment like
https://www.npmjs.com/package/node-fetch.
. The addition of the isomorphic-fetch solved this problem.

Writing our first Apollo GraphQL server

In the previous posts we’ve seen how to build an express based server and eventually add GraphQL to it.

graphql-express is not the only option for implementing GraphQL servers, let’s now look at Apollo – I cannot say whether one library is better than the other as I’ve not used them enough to comment although.

Let’s go through the process of creating a new project for this, so carry out the following steps

  • Create a folder for your project
  • cd to that folder
  • Run yarn init -y
  • Run tsc –init
  • Add a folder named models and a file named user.ts (we’ll use the same model/data from the previous posts), here’s the code for this file
    export default class User {
      constructor(public firstName: string, public lastName: string) {
      }
    }
    
    export const stubData = [
      new User('Scooby', 'Doo'),
      new User('Fred', 'Jones'),
      new User('Velma', 'Dinkley'),
      new User('Daphne', 'Blake'),
      new User('Shaggy', 'Rogers'),
    ];
    

So that’s the basics in place for the support data and model, let’s now look at the specifics for Apollo.

We’re going to create a file for the resolvers named resolver.ts and here’s the code

import User, { stubData } from "./models/user";

export const resolvers = {
  Query: {
    users: () => stubData,
  },
  Mutation: {
    create: (parent: any, args: any): User => {
      const data = new User(args.user.firstName, args.user.lastName)
      stubData.push(data);
      return data;
    }
  }
}

As you can see, we specify the Query and Mutation separately and the code’s pretty much the same as the express-graphql implementation except in the create mutation parameters. In this case the parent will supply any parent nodes whilst the args supplies the parameters from the mutation call.

Now we’ll create the file server.ts which will have the (as I’m sure you guessed) the server code as well as the schema definition (Apollo names this typeDefs).

import { ApolloServer, gql } from "apollo-server";
import { resolvers } from "./resolvers";

const typeDefs = gql`
type User {
    firstName: String
    lastName: String
  }

  input UserInput {
    firstName: String
    lastName: String
  }

  type Mutation {
    create(user: UserInput): User
  }

  type Query {
    users: [User]
  }
`;

const server = new ApolloServer({ typeDefs, resolvers });

server.listen()
  .then(({ url }) => {
    console.log(`Server listening to ${url}`);
  });

Note that Apollo uses a template tag gql function to declare the GraphQL schema, which along with the resolves is passed into the ApolloServer, then we simply start the server.

If you now run yarn build followed by yarn run the server will run up with default port 4000. Navigating to http://localhost:4000/ will display the GraphQL playground.

So, as you can see, Apollo is very simple to set-up for use as a GraphQL server.

Adding GraphQL to our Express server

In the previous two posts, we firstly create a basic server, then extended this with REST functionality, now lets add GraphQL capabilities.

First off, run the following

  • yarn add express-graphql graphql
  • Add a folder named schema

With the schema folder add a new file userSchema.ts with the following code

import { buildSchema } from "graphql";

export const schema = buildSchema(`
  type User {
    firstName: String
    lastName: String
  }

  type Query {
    users: [User]
  }
`);

In the above code, we create a GraphQL schema based upon our User type, along with the type Query for us to use the User type.

Next we’ll create a folder named resolvers along with the file userResolver.ts. GraphQL uses resolvers to interact with the data via GraphQL (like the code in the indexController in the previous post).

import { stubData } from "../models/user";

export const userResolver = {
  users: () => {
    return stubData;
  }
}

Now let’s update the server.ts file by adding the GraphQL code, I’ll include the whole file here (at least the specifics for GraphQL)

import graphqlHTTP from "express-graphql";
import { schema } from "./schema/userSchema";
import { userResolver } from "./resolvers/userResolver";

const server = express();

server.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: userResolver,
  graphiql: true,
}));

const port = 4000;
server.listen(port,
  () => console.log(`Server on port ${port}`)
);

In the above we’ve specified graphiql: true which allows us to via the GraphIQL page using http://localhost:4000/graphql

Now if you type the following into the left hand (query) pane

{
  users {
    firstName
    lastName
  }
}

Running this from GraphIQL should output all our stubData.

We’ve essentially got the users method from our original code but now let’s add the equivalent of the create method, which in GraphQL terms is a mutation type.

In the userSchema.ts file, change the schema to look like this

export const schema = buildSchema(`
  type User {
    firstName: String
    lastName: String
  }

  input UserInput {
    firstName: String
    lastName: String
  }

  type Mutation {
    create(user: UserInput): User
  }

  type Query {
    users: [User]
  }
`);

We’ve added an input type UserInput along with the type Mutation. Next, within the userResolve.ts file change the userResolve to add the create function, as below

export const userResolver = {
  users: () => {
    return stubData;
  },
  create: (data: any) => {
    stubData.push(new User(data.user.firstName, data.user.lastName)); 
    return data.user;
  }
}

Let’s test this code by executing the following query via GraphIQL

mutation {
  create(user: {
    firstName: "Miner",
    lastName: "FortyNiner",
  }) {
    firstName
    lastName
  }
}

Extending our Express based server

In my previous post I showed how to create an Express server in TypeScript. Let’s now extend things…

To start with, create some folders (the names are not important, in that this is not using convention based folder names). We’re going to set things up in a similar way to I’ve done for C# and Java…

  • Add a new folder controllers
  • Add a new folder models

Create a file server.ts in the root folder. Here’s the code from the previous post plus some extras

import express from "express";
import bodyParser from "body-parser";

const server = express();

server.use(bodyParser.json());

server.get("/", (request, response) => {
  response.send("<h1>Hello World</h1>");
});

const port = 4000;
server.listen(port, 
  () => console.log(`Server on port ${port}`)
);

The body-parser exposes middleware etc. in this case we’re adding the ability to work with JSON content type.

Now let’s change the existing route / and add some more, below is the additional code required in server.ts

import * as indexController from "./controllers/indexController";

server.get('/', indexController.index);
server.get('/users', indexController.users);
server.get('/users/create', indexController.create);

// POST implementation
// server.post('/users/create', indexController.create);

We now need to implement the model and the controller, so starting with the model, create the file user.ts within the models folder and it should look like this

export default class User {
  constructor(public firstName: string, public lastName: string) {
  }
}

export const stubData = [
  new User('Scooby', 'Doo'),
  new User('Fred', 'Jones'),
  new User('Velma', 'Dinkley'),
  new User('Daphne', 'Blake'),
  new User('Shaggy', 'Rogers'),
];

The stubData ofcourse is just here to give us some data to start things off.

In the controllers folder, add a new file named indexController.ts and the file should look like this

import { Request, Response } from "express";
import User, { stubData } from '../models/user';

export const index = (req: Request, res: Response) => {
  res.send("<h1>Methods are</h1><ul><li>users</li><li>create</li></ul>")
};

export const users = (req: Request, res: Response) => {
  res.json(stubData);
};

export const create = (req: Request, res: Response) => {
  const newUser = new User(req.query.firstName, req.query.lastName);
  stubData.push(newUser);
  res.json(newUser);
};

// POST implementation
// export const create = (req: Request, res: Response) => {
//   const newUser = new User(req.body.firstName, req.body.lastName);
//   stubData.push(newUser);
//   res.json(newUser);
// };

The scripts for package.json should also be taken from the previous post.

If you now run yarn build then yarn start your server will be up and running. Using the following URL’s

  • http://localhost:4000/
  • http://localhost:4000/users
  • http://localhost:4000/users/create?firstName=Miner&lastName=FortyNiner
  • will result in, the first URL returning a simple help screen using HTML, the second will list the current array of users using JSON and finally the third URL will create a new user, the response will show the new user as JSON, but if you call the users method again you will see the newly added user.

    I’ve also listed the POST implementations which you’d probably more likely use for mutations, but the GET implementations are fine in this instance and easier to test via our preferred browser.

Express server with TypeScript

Express is a popular web application server in JavaScript.

Let’s create a really quick and simple expression server which will be a good starting point for further posts which use this library.

I’ll assume you’ve created a folder, run yarn init and tsc –init, next up run

  • yarn add express
  • yarn add -D @types/express
  • Add a new file, mine’s named server.ts
  • Add the usual scripts
    "scripts": {
      "build": "tsc",
      "start": "node server.js"
    }
    

The code to run up a really simple server is as follows

import express from "express";

const server = express();

server.get("/", (request, response) => {
  response.send("<h1>Hello World</h1>");
});

const port = 4000;
server.listen(port, 
  () => console.log(`Server on port ${port}`)
);

Using our scripts, run yarn build followed by yarn start. The server should start on port 4000, so now navigate to http://localhost:4000 using your preferred browser and that’s it.

We’ve created a server instance on port 4000, we’re routed any root (i.e. /) calls to the response, which in this case returns a simple HTML string.

The “is” type guard in TypeScript

The is keyword can be used to narrow types in TypeScript. What this means is that, for example if we’re testing whether an any parameter is a string a number or whatever, TypeScript will see the type as that type in subsequent code.

For example if we have the following functions

function isString(test: any): test is string {
  return typeof test === "string";
}

function isNumber(test: any): test is number {
  return typeof test === "number";
}

Each function simply takes a parameter of type any then if the return is true the parameter in subsequent code is seen by TypeScript as that type, i.e. Let’s assuming we have a some function which happens to use these functions, like this

function run(a: any) {
  if(isString(a)) {
    console.log(a)
  }
  if(isNumber(a)) {
    console.log(a)
  }
}

If you’re using Visual Code (for example) the TypeScript type popup (display when you move the mouse over the parameter a will show the type to be any in the run parameter list, or in any of the isXXX function parameters. But in the console.log after each isXXX function, TypeScript will interpret the type to be a string (in the case of isString) and a number (in the case if isNumber) hence using intellisense on the variables will list the methods etc. for a string and those for a number respectively

So basically the is keyword will appear to convert the parameter to the specified type if the function returns true.

To look at it another way we could instead write something like this

function isString(test: any): boolean {
    return typeof test === "string";
}

function isNumber(test: any): boolean {
    return typeof test === "number";
}

function run(a: any) {
  if(isString(a)) {
    const b = a as string;
    console.log(b)
  }
  if(isNumber(a)) {
    const b = a as string;
    console.log(b)
  }
}

For completeness, here’s the function syntax for a const style function

const isString = (test: any): test is string => {
  return typeof test === "string";
}

Simple node based HTTP server

In a previous post we used webpack to run an HTTP server, in this post we’re going to create a bare bones HTTP server using the http package.

In your chosen folder run the usual commands

  • yarn init y
  • tsc –init
  • Add a folder named public off of the folder you source will be in (this will be where we add static html files)

Now lets’ add the required packages

  • yarn add node-static @types/node-static

Add the following to the package.json

"scripts": {
  "start": "node server.js",
  "build": "tsc"
}

In the public folder add index.html with the following

<html>
  <head></head>
  <body>
  Hello World
  </body>
</html>

Now let’s add the code to start up our server (mine’s in the file server.ts)

import ns from "node-static";
import http from "http"

const file = new ns.Server("./public");

http.createServer((request, response) => {
    request.addListener("end", () => {
        file.serve(request, response);
    })
    .resume()
})
.listen(4000);

Simple run yarn build then yarn start and the server will start. Navigating your preferred browser to http://localhost:4000/ will then display the HTML file, i.e. Hello World text.

Websockets with JavaScript

Let’s create a JavaScript websocket server.

To start with carry out the following steps…

  • Create a folder, mine’s wsserver and cd to it
  • Run yarn init -y
  • Run tsc –init
  • Run yarn add ws @types/ws
  • Add a file (mine’s server.ts) for our server code
  • Add another file this time for a sample client (mine’s client.ts)
    • Now we will add the scripts, so add the following to package.json

      "scripts": {
        "server": "node server.js",
        "client": "node client.js"
        "build": "tsc"
      }
      

      Let’s add some code, in the server.js put the following

      import WebSocket from "ws";
      
      const wss = new WebSocket.Server({ port: 4000 });
      
      wss.on("connection", ws => {
        ws.on('message', message => {
          console.log('received: %s', message);
        });
      
        ws.on("close", () => {
          console.log("Close connection");
        });
      
        ws.send("Server says Hi");
      });
      

      In the above code we create a new websocket server on port 4000 then we handle any connection’s (once a connection is made the server sends back the message “Server says Hi”. The ws.on “message” will output any messages sent from the client. It should be fairly obvious that the “close” is called when a connection is closed.

      Let’s now put the following in the client.ts

      import WebSocket from "ws";
      
      const ws = new WebSocket("ws://localhost:4000");
      
      ws.onopen = () => {
        ws.send("Client says Hi");
      };
      
      ws.onerror = error => {
        console.log(error);
      }
      
      ws.onmessage = msg => {    
        console.log(`Client onmessage: ${msg.data}`);
      }
      
      ws.onclose = () => {
        console.log("Close");
      };
      

      In the above we open the connection to the web socket server, when the connection is open (onopen) we send a message “Client says Hi” to the server, obviously any errors are sent to the onerror function and any message from the server are routed to the onmessage function, finally onclose is called if the server closes the connection.

      Now run the script command yarn build and then in one terminal run yarn server and in another terminal run yarn client.

      We can also send specific commands to the server, for example in the client add the following to the onopen function

      ws.send("getData");
      

      Within the server add the following

      ws.on("getData", msg => {
        console.log("getData called")
      });
      

      So now when the server receives a getData messages it’s routed to this server function.

      If we have multiple client’s connecting to a server, we can send messages to each client using code, like the following

      wss.clients.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
          client.send("Broadcasting a  message);
        }
      });
      

      We can also extend the server “connection” function like this

      wss.on("connection", (ws, request) => {
        console.log(request);
      }
      

      The request parameter allows us to check the request.url if we want to change take different actions depending upon the query part of the websocket URL.

      It’s often useful to implement ping/pong capabilities which would allow us to check if the client still exists, here’s rather simplistic example of this type of code.

      wss.on("connection", (ws, request) => {
        ws.isAlive = true;
      
        ws.on("pong", () => {
          console.log("Pong called");
          ws.isAlive = true;
        });
      
        setInterval(function ping() {
          wss.clients.forEach(function each(ws: any) {
            if (ws.isAlive === false)  {
              console.log("Terminated client")
              return ws.terminate();
            }
              
            ws.isAlive = false;
            console.log("Ping client")
            ws.ping();
          });
        }, 10000);
      });
      

Be aware that an async (in TypeScript) when not required adds extra code

Whilst aysnc/await is not formally part of the ES spec (I believe it is available when targetting es2017) we need to be aware of situations where declaring async adds code which is not required.

Specifically I came across some code where a class’s method was marked as async but without an implementation, i.e.

class Derived {
    public async doSomething(): Promise<void> {
    }
}

This compiled/transpiled with the tsconfig.json settings of the project (a React project) and will produce the following code

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class Derived {
    doSomething() {
        return __awaiter(this, void 0, void 0, function* () {
        });
    }
}

Obviously is not the intention here, i.e. to include the __awaiter code etc. What the code should have looked like is

class Derived {
    public dataBind(): Promise<void> {
        return Promise.resolve();
    }
}

which then produces JavaScript matching exactly this code, i.e.

"use strict";
class Derived {
    dataBind() {
        return Promise.resolve();
    }
}

I know this might seem a little picky, but from what I could see, for every file that includes an empty async method we get an __awaiter created, considering we use minifiers etc. to try to reduce our code size, this obviously makes a difference when code size is important.

JavaScript tagged templates

If you’ve seen code such as the one below (taken from https://www.styled-components.com/docs/basics)

styled.section`
  padding: 4em;
  background: papayawhip;
`

You might be interested in the styled.section code. This is a function and uses template literals as input via template literal syntax. In this usage its known as a tagged template or tagged template literals.

Let’s create our own function to show how this works.

function tt(literals: any, …substitutions: any) {
console.log(literals);
console.log(substitutions);
}

Note: I’m using TypeScript, hence the use of the any keyword, but just remove this for JavaScript code.

If we now run the following code

const name1 = "Scooby";
const name2 = "Doo";

tt`Hello ${name1} World ${name2}`

The following will be logged to the console

[ 'Hello ', ' World ', '' ]
[ 'Scooby', 'Doo' ]

The first values are an array of the literals passed to our function, the second are the substitutions.

Literals will always be an array of substitutions.length + 1 in length. Hence in the example above the literals contains an empty string item at the end to ensure this is the case.

Note: The last item in the literals array is an empty string but ofcourse if we had a string after the ${name2} then this would be the last item, hence to combine these two arrays into a resultant string would require us to ensure we merge all items.

We can therefore combine our two arrays to form a single result using a simple loop, like this


function tt(literals: any, ...substitutions: any) {
  let s = "";

  for (let i = 0; i < substitutions.length; i++) {
    s += literals[i] + substitutions[i];
  }

  return s + literals[literals.length - 1];
}

In the above we’re simply returning a string representing the merge of the two arrays. Remember literals.length will be substitutions.length + 1, hence we simply append that after looping through the smaller of the arrays.

Ofcourse this it not really that useful, if all we wanted to do was return a string we could just create a template literal. Let’s look at a couple of ways of enhancing the functionality.

The first obvious requirement is that we should be able to pass functions into the templates. For example if we have something like this

const t = tt`
 firstName: ${name1};
 lastName: ${name2};
 preferred: ${choice => (choice ? name1 : name2)};
 `;

The choice value needs to be supplied by the calling code and in this example code there’s no easy was to pass this data into t. So first off we need to wrap the tt function within another function and return it, like this

 
function tt(literals: any, ...substitutions: any) {
  return function(options: any) {
    let s = "";

    for (let i = 0; i < substitutions.length; i++) {
      s += literals[i];
      s += typeof substitutions[i] === "function"
        ? substitutions[i](options)
        : substitutions[i];
    }
    return s + literals[literals.length - 1];
  };
}

In the above we’ve also added changes to the original tt function to detect functions within the substitutions. If a function is found whilst looping then it’s invoked by passing in the supplied options.

This implementation then returns a function which, when invoked by passing in some value (in this case named options), will loop through the literals and substitutions and invoking any functions by forwarding the supplied options.

Hence we can call the new tt method like this, for example

t({choice: true});

This would return a string and would return

firstName: Scooby;
lastName: Doo;
preferred: Scooby;

So now for the next enhancement, let’s instead of returning a string, return an object – all we need to do is split on semi-colons to get key/value items where the key will become the object’s property and the value obviously the value stored within the property.

We’ll make a slight change to the code above to this

function tt(literals: any, ...substitutions: any) {
  return function(options: any) {
    let s = "";

    for (let i = 0; i < substitutions.length; i++) {
      s += literals[i];
      s += typeof substitutions[i] === "function"
        ? substitutions[i](options)
        : substitutions[i];
    }

    return toObject(s + literals[literals.length - 1]);
  };
}

The toObject function has been introduced and it’s purpose is to…

  • Take a string which is semi-colon deliminated for each key/value pair
  • Extract each key/value pair which should be deliminated with colons
  • For each entry we will create a property with the name taken from left of the colon on an object and the value right of the colon will be assigned to the property as a value

Here’s the code for toObject

const toObject = (value: any): any =>
  value
    .split(";")
    .map(entry => {
        const e = entry.split(":");
        if(e.length == 2) {
            const key = e[0].trim();
            const value = e[1].trim();
            return [key, value];
        }
        return undefined;
    })
    .filter(entry => entry != undefined)
    .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1]}), {});

This is not a complete solution as we’re not ensuring validity of the key as a property name. For example you’ll have noticed in styled.component or even React’s styles, that hyphen keys, i.e. background-color or similar would be converted or expected to be backgroundColor. So a simply change would be to convert line 7 to this following

const key = ensureValid(e[0].trim());

and now we introduce a new function to handle all our checks, for now we’ll just ensure the hyphen’s or dot’s are removed and replaced by camelCase

const ensureValid = (key: string): string => 
 key.replace(/[-.]+/g, c => c.length > 0 ? c.substr(1).toUpperCase() : '');

Obviously this function is quite limited, but you get the idea. It can then be used in the toObject function, i.e.

// change
const key = e[0].trim();
// to
const key = ensureValid(e[0].trim());

Taking things a little further

The code below is based upon what was discussed in this post, but extended a little, to start with here’s a more complete implementation of the above code

const ensureValid = (key: string): string => 
    key.replace( /[-.]+([a-z]|[0-9])|[-.]$/ig, (_match, character, pos) => {
        if(pos == 0) {
            return character.toLowerCase();
        }
        else if(character == null) {
            return '';
        }

        return character.toUpperCase();
    });

const toObject = (value: any): any =>
  value
    .split(";")
    .map(entry => {
        const e = entry.split(":");
        if(e.length == 2) {
            const key = ensureValid(e[0].trim());
            const value = e[1].trim();
            return [key, value];
        }
        return undefined;
    })
    .filter(entry => entry != undefined)
    .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1]}), {});

function tt3(literals: any, ...substitutions: any) {
    return function(options: any) {
        let s = "";

        for (let i = 0; i < substitutions.length; i++) {
            s += literals[i];
            s += typeof substitutions[i] === "function"
                ? substitutions[i](options)
                : substitutions[i];
        }

        return toObject(s + literals[literals.length - 1]);
    };
}

const name1 = "Scooby";
const name2 = "Doo";

const t = tt3`
 -First-6name-: ${name1};
 last-Name: ${name2};
 preferred: ${options => (options.choice ? name1 : name2)};
 `;

console.log(t({choice: true}));

Now let’s have a bit of fun and refactor things to allow us to extract our object from alternate data representations. We’ll create an ini style way to define our objects

const camelCase = (key: string): string => 
    key.replace( /[-.]+([a-z]|[0-9])|[-.]$/ig, (_match, character, pos) => {
        if(pos == 0) {
            return character.toLowerCase();
        }
        else if(character == null) {
            return '';
        }

        return character.toUpperCase();
    });

type splitterFunc = (value: any) => [{key: any; value: any}|undefined];

const standardSplitter = (value: any):  [{key: any; value: any}|undefined] =>
    value
        .split(";")
        .map(entry => {
            const e = entry.split(":");
            if(e.length == 2) {
                const key = camelCase(e[0].trim());
                const value = e[1].trim();
                return [key, value];
            }
            return undefined;
        });

const iniSplitter = (value: any):  [{key: any; value: any}|undefined] =>
        value
            .split("\n")
            .map(entry => {
                const e = entry.split("=");
                if(e.length == 2) {
                    const key = camelCase(e[0].trim());
                    const value = e[1].trim();
                    return [key, value];
                }
                return undefined;
            });
    

const toObject = (value: any, splitter: splitterFunc = standardSplitter): any =>
    splitter(value)
      .filter(entry => entry != undefined)    
      .reduce((obj, entry) => ({ ...obj, [entry![0]]: entry![1]}), {});


function tt3(literals: any, ...substitutions: any) {
    return function(options: any, splitter: splitterFunc = standardSplitter) {
        let s = "";

        for (let i = 0; i < substitutions.length; i++) {
            s += literals[i];
            s += typeof substitutions[i] === "function"
                ? substitutions[i](options)
                : substitutions[i];
        }

        return toObject(s + literals[literals.length - 1], splitter);
    };
}

const name1 = "Scooby";
const name2 = "Doo";
    
const t = tt3`
 -First-6name-: ${name1};
 last-Name: ${name2};
 preferred: ${options => (options.choice ? name1 : name2)};
 `;

 const t1 = tt3`
 -First-6name- = ${name1}
 last-Name = ${name2}
 preferred = ${options => (options.choice ? name1 : name2)}
 `;

console.log(t1({choice: true}, iniSplitter));