Saturday, April 08, 2023

Strongly Typed String Literal Split-Map-Join in TypeScript

The Problem: Write a strongly typed TypeScript function that takes a valid CSS property name in kebab-case (used in CSS) and return the same property name in camelCase (usually used in CSS-in-JS). For example, call this function with "font-size" and it should return "fontSize".

The Strongly Typed String Literal Requirement: A TypeScript type that contains all valid kebab-case CSS property names is provided. Make sure TypeScript can infer to correct camelCase output from this function. To explain it in code:

type kebabCasePropertyName =
  | 'align-content' 
  | 'align-items'
  | 'align-self'
  | 'background'
  | 'background-attachment'
  | 'background-color'
  | 'background-image'
  /* remaining valid property names */

function convert(propertyName: kebabCasePropertyName) /*: define return type */ {
  /* implement function */
}

const camelCasePropertyName = convert('align-content');
// typeof camelCasePropertyName should be 'alignContent'

convert('invalid-property-name');
// TypeScript should throw a compile time error

Here is a TypeScript Playground with the same code. If you want to try solving this problem yourself, go ahead and try it out. You may find a solution better than the one I’m going to share below.

The Solution: This is the TypeScript Playground with the solution code. Now we can go into understanding how it works.

type Split<S, D>

This generic type splits string literal S with string literal delimiter D.

type Split<S extends string, D extends string>
  = string extends S ? Array<string>
  : S extends '' ? []
  : S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>]
  : [S];

Here S extends string means S has to be a string. The more precise definition is S has to be a subset of all possible string values. The same applies to D, so S and D have to be strings or TypeScript will throw a compile time error.

The next few lines use the ternary conditional operator several times to pattern match different possible types of S. The following pseudo-code may help you follow the logic:

type Split<S extends string, D extends string>
if (string extends S) then return Array<string>
else if (S extends '') then return []
else if (S extends `${infer T}${D}${infer U}`) then return [T, ...Split<U, D>]
else return [S];

First, we try to match string extends S. If it passes, that means S isn’t a string literal. (Previously we already knew S is a subset of string. If string is also a subset of S then S is exactly string. Nothing more. Nothing less.) It’s a string and its value is unknown at compile time. There’s nothing we can do here. Split<S, D> can only be narrowed down to Array<string>.

Then we try to match S extends ''. It just means S is an empty string because the subset of empty string is just an empty string. Then we can narrow Split<S, D> down to an empty array.

And then we try to match S extends${infer T}{$D}${infer U}`. There are two concepts we need to understand here:

  1. TypeScript template literal types. When using JavaScript template literals, TypeScript can infer all the possible string interpolation outcomes.
  2. The infer keyword. It can only be used after the extends keyword. It can be used to deconstruct a type that’s constructed from other types.

So here we try to deconstruct S into template literal type `${infer T}${D}${infer U}`. For example, Split<'hello-world', '-'> has D extends '-', so it can be deconstructed into T extends 'hello' and U extends 'world', because ${T}${D}${U} will construct 'hello-world'. By using infer, we ask TypeScript to figure out T and U for us.

If the deconstructing works. we can narrow down Split<S, D> into [T, ...Split<U, D>]. This is very similar to how we would implement a JavaScript split function with recursion:

function split(string, delimiter) {
  const index = string.indexOf(delimiter);
  return index >= 0 
    ? [
      string.substring(0, index),
      ...split(string.substring(index + 1), delimiter)
    ]
    : [string];
}

If the deconstructing doesn’t work, the last line in the pseudo-code is just like the last line inside the JavaScript above. It means S is a string literal but it doesn’t contain D, so we return [S]. We can see the similarity between JavaScript and TypeScript type expressions.

type Join<A, D>

This is like reversing Split<S, D>, in a very similar recursive manner. `${T}${D}${Join<U, D>}` represents that recursion.

type Join<A extends Array<string>, D extends string>
  = A extends [] ? ''
  : A extends [infer T extends string] ? `${T}`
  : A extends [infer T extends string, ...infer U extends Array<string>] ? `${T}${D}${Join<U, D>}`
  : string;

Split<S, D> requires S and D to be string literals. Join<A, D> requires A to be an array literal and all of its elements are string literals. If A doesn’t satisfy these requirements, we can only narrow Join<A, D> down to string.

Here we use template literal type `${T}${D}${Join<U, D>}` to construct one string type from multiple string types. This is the opposite operation of how we deconstruct in Split<S, D>.

type LowercaseArray<A>>

Again we are using recursion to iterate through an array. This is similar to Join<A, D>. However, we don’t return a template literal type. We return a new array type that contains new string literal types.

type LowercaseArray<A extends Array<string>>
  = A extends [] ? []
  : A extends [infer T extends string, ...infer U extends Array<string>] ? [Lowercase<T>, ...LowercaseArray<U>]
  : A;

TypeScript has a built-in Lowercase<T> that returns the string literal type of the lower case of another string leteral type. We don’t need to do this ourselves.

type CapitalizeArray<A>

It’s very similar to LowercaseArray<A>. We use the built-in Capitalize<T> to capitalize the first letter of a string literal type.

type CapitalizeArray<A extends Array<string>>
  = A extends [] ? []
  : A extends [infer T extends string, ...infer U extends Array<string>] ? [Capitalize<Lowercase<T>>, ...CapitalizeArray<U>]
  : A;

type CamelCaseArray<A>

camelCase has first word in all lowercase and subsequent words in lowercase with first character capitalized. This can be achieved by combining LowercaseArray<A> and CapitalizeArray<A> into a new type CamelCaseArray<A>.

In the end, we can combine this type of Split<S, D> and Join<A, D> to create CamelCase<S>. It’s just like how we would implement this as a JavaScript function: a chained split-map-join operation.

function convert(propertyName) {
  return propertyName
    .split('-')
    .map((word, index) => index === 0
      ? word
      : `${word.charAt(0).toUpperCase()}${word.substring(1)}`)
    .join('');
}

How about the opposite operation? How can we create a TypeScript generic type that converts camelCase back to kebab-case? That’s an exercise for you. There’s no clear delimiter like '-' in this operation. Think about how you would do it in JavaScript and use pattern matching in TypeScript to achieve the same result.

Friday, June 19, 2020

Job Promotion: Scaling-bound or Opportunity-bound?

This is my thought after reading a conversation on how promotion to some levels is harder than some other levels in Facebook. The promotion is easier when the next level is the same job as your current level but a more mature version. It’s harder when the next level is a different job that requires new skills.

Based on this I would break down promotion requirements into two categories:

  1. Scaling your current skills.
  2. Learning and practicing new skills.

Promotions that mostly require the first category are the easier ones. The next level is more or less the same job. Let’s call it scale-bound promotion. Promotions that heavily involves the second category are the harder ones. The next level is like a different job. I’ll call it opportunity-bound promotion.

The first step to optimize your promotion is to identify whether your next level is scale-bound or opportunity-bound. In the conversation that I read about, people divided Facebook levels into buckets – [3, 4], [5], [6, 7], [8, 9] – and it’s the same job in the same bucket. Crossing the buckets requires you to learn and practice new skills because it’s a different job.


Opportunity-bound promotion is harder because you need opportunities to learn and practice new skills that are required by your next level while your current level doesn’t provide such opportunities. Usually, those opportunities come to you naturally when you are at your next level. This becomes a chicken and egg problem – you are not at the next level so you don’t get opportunities required by your next level.

Even if the promotion to your next level is opportunity-bound, you need to deal with the scale-bound part well first. Otherwise, when someone offers you an opportunity you might fail because your current skills don’t scale enough. That person will regret offering you the opportunity. The next opportunity will be harder to come by. It’s better to make sure your current skills are mostly scaled to meet the expectation of your next level first.


When you are confronted with an opportunity-bound situation, the best-case scenario is having a great manager. The manager should connect you with the right opportunities to develop the skills you don’t have. These opportunities should push you out of your comfort zone but not too far away. That’s the tailwind setup. You are in good hands.

The meh scenario is when the team is functioning okay but your manager isn’t actively optimizing the part of the team that you are in. That means your manager isn’t actively helping you with new opportunities. This scenario is common when your manager is too aggressive and spread himself too thin. It’s also common when your manager is complacent and doesn’t want to further develop the team.

You need to have better than average (comparing to your peers at your level) soft skills to build relationships with a broader group of people in your company. Learn about the broader business your team is in and discover opportunities yourself. You might also need to perform better than your peers when there are fewer opportunities than the people who are qualified.

The worst-case scenario is when the team is malfunctioning. People don’t know what they are supposed to do or what they can do to meaningfully help the team.

It’s like war. There are casualties. People can’t perform well because the team is broken but they got fired anyway. People who don’t get fired will realize the situation they are in and might jump ship as fast as they could. Interestingly, war heroes only emerge from the war. Field promotion can fast track you at a speed that’s not achievable in peacetime. Opportunities open up when there are casualties and deserters.

Tuesday, November 26, 2019

Front-End Learning for Programmer

If you are an existing programmer (fluent in one common programming language) and want to learn Front-End (HTML+CSS+JS), I would recommend using freeCodeCamp and picking only the modules you need. If you want to learn just enough to work on modern front-end projects or start a new project with Create React App, below is what modules I think you should learn on freeCodeCamp.

“✔️” means you should learn it. “❌” means you could skip it. “❗” means you could skip if you are in a hurry but you should learn if you have time.

  • Responsive Web Design
    • ✔️ Basic HTML and HTML5
    • ✔️ Basic CSS
    • ✔️ Applied Visual Design
    • ❌ Applied Accessibility
    • ❌ Responsive Web Design Principles
    • ✔️ CSS Flexbox
    • ❌ CSS Grid
    • ✔️ Responsive Web Design Projects
  • JavaScript Algorithms and Data Structures
    • ✔️ Basic JavaScript
    • ✔️ ES6
    • ❌ Regular Expressions
    • ✔️ Debugging
    • ✔️ Basic Data Structures
    • ❗ Basic Algorithm Scripting
      • (Use it as a practice to write more JavaScript code.)
    • ✔️ Object Oriented Programming
    • ✔️ Functional Programming
    • ❗ Intermediate Algorithm Scripting
      • (Use it as a practice to write more JavaScript code.)
    • ❗ JavaScript Algorithms and Data Structures Projects
      • (Use it as a practice to write more JavaScript code.)
  • Front End Libraries
    • ✔️ Bootstrap
    • ❌ jQuery
    • ❌ Sass
    • ✔️ React
    • ✔️ Redux
    • ✔️ React and Redux
    • ✔️ Front End Libraries Projects

Tuesday, November 12, 2019

Progressive Web App as Share Target on Android

I built a PWA (Progressive Web App) to trace shortened URL back to its original URL. (You can find it here.) I don’t want to copy a URL and paste it into my app. I want to use the Android’s sharesheet to send any link in Chrome straight to my app. How do I do that?

Google provides good documentation on this. We need to add a share_target section in manifest.json and then declare that our PWA can act as a share target. Most of the properties in this section can be thought of as attributes on a <form> element with the same name. For example, { "action": "/share", "method": "POST" } is like <form action="/share" method="POST">.

params subsection let us change parameter names if we already have a convension of naming search parameters in GET requests or form fields in POST requests. Otherwise, we can keep them in their original names. One caveat is Android doesn’t use url parameter so when sharing a URL it comes through the text parameter. In my app I need to coalesce these two parameters to get the input from the user.

Is there more? Yes! Twitter makes a great PWA and we can check their manifest.json. Here’s the beautified version of the share_target section:

"share_target": {
  "action": "compose/tweet",
  "enctype": "multipart/form-data",
  "method": "POST",
  "params": {
    "title": "title",
    "text": "text",
    "url": "url",
    "files": [
      {
        "name": "externalMedia",
        "accept": [
          "image/jpeg",
          "image/png",
          "image/gif",
          "video/quicktime",
          "video/mp4"
        ]
      }
    ]
  }
}

It has a files subsection under the params section. This is part of the Web Share Target Level 2. We can accept files from sharesheet and we can assign a file to different parameter name based on MIME type or file extension. My app doesn’t need this capability but it’s good to know what’s possible.

If you like my post, you can subscribe through email or RSS/Atom. That makes sure you won’t miss my future posts.

Wednesday, November 06, 2019

Batch Sending Email with Attachments through AppleScript

I want to learn a little bit of AppleScript. I need to help a friend send out emails to welcome new students to the school. The requirements are:

  1. addressing each recipient by their name in email content;
  2. attaching the same file in all emails.

After some research and tinkering I have a script to send emails:

theRecipients is the list of recipient names and email addresses. This is for requirement #1. theAttachment isn’t hardcoded to any file path. It will prompt and let me choose a file when I run the AppleScript.

The trickiest part is the delay 1. Without this line, emails will be sent without the attachment. It’s a hack to make sure each email has the attachment. I don’t know why it works and I can’t find an explanation online.

After building this AppleScript, I learn that Google App Script is another great way to automate sending emails through Gmail (or GSuite). I will learn App Script and write a post about that next time. If you like this kind of posts, you can subscribe through email or RSS/Atom.

Tuesday, September 24, 2019

MailChimp Popup Dialog on Click

Problem

MailChimp provides sign-up form as pop-up dialog, but has limited options for when to trigger it. Available triggers are like “immediately after the page is open”, “when the user scrolls to the bottom of the page”, etc. I want to trigger the dialog when a reader clicks the sign-up link on my blog. Instead of navigating to the sign-up page, I want to open the dialog and speed up the experience. I hope this can improve subscription rate.

Solution

MailChimp provides an HTML snippet for the pop-up dialog. It contains two <script> tags. I left the first one untouched and modified the second one. Read the code below for reference:

In the first <script> tag, I did nothing. In the second <script> tag, I wrapped the original JavaScript in a function called displayDialog, which will be called in the sign-up link’s click event. I added a third <script> tag to search for sign-up links in the page and add the click event handler.

Within displayDialog function, I not only call the original code in the second <script> tag but also delete two cookies before that. That’s because MailChimp set one of these cookies when a user dismisses the dialog or subscribes through the dialog. The presence of one of the cookies will prevent the dialog from opening again. This behavior makes sense when using MailChimp’s automatic triggers – a user shouldn’t see a dialog again after either dismissing or subscribing. It doesn’t make sense when the trigger is user clicking a link because it’s a clear intention to open the dialog. (If you want to reuse my code, remember to replace window.dojoRequire(...) with the code from your own MailChimp campaign.)

The third <script> tag is customized for my own blog. It looks for any link that points to the sign-up page and add the click event handler. The event handler calls displayDialog to trigger the dialog and then cancels the browser’s navigation to the sign-up page.

If you want to test this feature, make sure you open this post from my blog and click this link to subscribe. It should open the sign-up dialog instead of the sign-up page. If you like this post, remember to put in your email and subscribe!

Wednesday, August 28, 2019

Is Targeted Ads Price Discrimination?

If you use a service for free but you need to see ads targeting towards you, how much do you actually pay? I didn’t think about this before I used Google Contributor a few years ago. Now I think I pay the whatever the price advertisers pay to show me the ads, but then the advertisers subsidize me fully to get me to see their ads.

The concept of Google Contributor is very simple. You can understand it if you ever purchased targeted ads online. You set up a budget between $2 and $15 per month. You use that budget to buy ads targeting a single person – yourself. You compete against any advertisers that happen to target you. Every time when you are supposed to see an ad, the bidding process happens between you and other advertisers. If you win, you see a message saying “thank you for being a contributor” in the place where the ad should be displayed. If you lose, you see the ad from the winning advertiser, just like if you were not using Google Contributor.

I would imagine I win in every case so I never see any ads. The cost I pay for that is the price I actually pay for using the “free” service. When I don’t win or when I don’t use Google Contributor altogether, it’s advertisers subsidizing me but I have to see their ads, but it’s still the same price.

It’s interesting that different user pays different price. It’s like price discrimination. If you are worth more in the eyes of advertisers, you pay more. If your impression is worth less, you pay less. It’s easy to guess that an average user in the US would pay more than an average user in India.

If we apply similar price discrimination to a subscription based service (e.g. Microsoft Office 365, which is cheaper in China), we need to use IP check or other methods to prevent customers in a higher price region from purchasing from a lower price region. There’s no such need when the service is paid by targeted ads. A user in the US can’t pretend to be in India and then “pay” less by seeing lower cost ads. It’s also not in the US user’s interest to do so, because ads targeting an Indian user is less relevant and doesn’t improve experience.

In conclusion, I think targeted ads is an effective form of price discrimination. What do you think? (Feel free to comment after the post.)