Simple object lookup in TypeScript

June 14, 2024
0 comments JavaScript

Ever got this error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: string; bar: string; }'. No index signature with a parameter of type 'string' was found on type '{ foo: string; bar: string; }'.(7053)

Yeah, me too. What used to be so simple in JavaScript suddenly feels hard in TypeScript.

In JavaScript,


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

To see it in action, I put it into a CodePen.

Now, port that to TypeScript,


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

Same. Except it doesn't work.
You can view it here on the TypeScript playground

This is the error you get about greetings[answer]:

TypeScript playground with error

Full error:

Element implicitly has an 'any' type because the expression of type 'string' can't be used to index type '{ good: string; bad: string; }'. No index signature with a parameter of type 'string' was found on type '{ good: string; bad: string; }'.(7053)

The simplest way of saying is that that object greetings, does not have any keys that are type string. Instead, the object has keys that are exactly good and bad.

I'll be honest, I don't understand the exact details of why it works like this. What I do know is that I want the red squiggly lines to go away and for tsc to be happy.
But what makes sense, from TypeScript's point of view is that, at runtime the greetings object can change to be something else. E.g. greetings.bad = 123 and now greetings['bad'] would suddenly be a number. A wild west!

This works:


const greetings: Record<string, string> = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
  alert(greetings[answer] || "OK")
}

All it does is that it says that the greetings object is always a strings-to-string object.

See it in the TypeScript playground here

This does not work:


const greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
  alert(greetings[answer as keyof greetings] || "OK")  // DOES NOT WORK
}

To be able to use as keyof greetings you need to do that on a type, not on the object. E.g.
This works, but feels more clumsy:


type Greetings = {
  good: string
  bad: string
}
const greetings: Greetings = {
  good: "Excellent",
  bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
  alert(greetings[answer as keyof Greetings] || "OK")
}

In conclusion

TypeScript is awesome because it forces you to be more aware of what you're doing. Just because something happen(ed) to work in JavaScript, when you first type it, doesn't mean it will work later.

Note, I still don't know (please enlighten me), what's the best practice between...


const greetings: Record<string, string> = {

...versus...


const greetings: {[key:string]: string} = {

The latter had the advantage that you can give it a name, e.g. "key".

Search GitHub issues by title, only

May 31, 2024
0 comments GitHub

tl;dr You can search GitHub issues specifically only in the title by adding in:title.

Suppose you go to a GitHub repository's Issue list. If you search, it will find issues that match your search in any field. For example:

Finding 70 issues by dialog

70 issues found

Now, add in:title to the search input:

Finding 30 issues by dialog when only searching in the title

Only 30 GitHub issues found

I use this all the time when I know that the search input has to be in the title. Especially when you know what you're looking for.

Perhaps I should implement this on my own blog?

UPDATE

My friend @andyfeller reminded me that this page has all the "tricks" listed:
"Searching issues and pull requests"

How do you thousands-comma AND whitespace format a f-string in Python

March 17, 2024
1 comment Python

For some reason, I always forget how to do this. Tired of that. Let's blog about it so it sticks.

To format a number with thousand-commas you do:


>>> n = 1234567
>>> f"{n:,}"
'1,234,567'

To add whitespace to a string you do:


>>> name="peter"
>>> f"{name:<20}"
'peter               '

How to combine these in one expression, you do:


>>> n = 1234567
>>> f"{n:<15,}"
'1,234,567      '

Leibniz formula for π in Python, JavaScript, and Ruby

March 14, 2024
0 comments Python, JavaScript

Officially, I'm one day behind, but here's how you can calculate the value of π using the Leibniz formula.

Leibniz formula

Python


import math

sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while abs(estimate - math.pi) > epsilon:
    sum += (-1) ** i / (2 * i + 1)
    estimate = sum * 4
    i += 1
print(
    f"After {i} iterations, the estimate is {estimate} and the real pi is {math.pi} "
    f"(difference of {abs(estimate - math.pi)})"
)

Outputs:

After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)

JavaScript


let sum = 0;
let estimate = 0;
let i = 0;
const epsilon = 0.0001;

while (Math.abs(estimate - Math.PI) > epsilon) {
  sum += (-1) ** i / (2 * i + 1);
  estimate = sum * 4;
  i += 1;
}
console.log(
  `After ${i} iterations, the estimate is ${estimate} and the real pi is ${Math.PI} ` +
    `(difference of ${Math.abs(estimate - Math.PI)})`
);

Outputs

After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 0.0000999999997586265)

Ruby


sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while (estimate - Math::PI).abs > epsilon
    sum += ((-1) ** i / (2.0 * i + 1))
    estimate = sum * 4
    i += 1
end
print(
    "After #{i} iterations, the estimate is #{estimate} and the real pi is #{Math::PI} "+
    "(difference of #{(estimate - Math::PI).abs})"
)

Outputs

After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)

Backwards

Technically, these little snippets are checking that it works since each language already has access to a value of π as a standard library constant.

If you don't have that, you can decide on a number of iterations, for example 1,000, and use that.

Python


sum = 0
for i in range(1000):
    sum += (-1) ** i / (2 * i + 1)
print(sum * 4)

JavaScript


let sum = 0;
for (const i of [...Array(10000).keys()]) {
  sum += (-1) ** i / (2 * i + 1);
}
console.log(sum * 4);

Ruby


sum = 0
for i in 0..10000
    sum += ((-1) ** i / (2.0 * i + 1))
end
puts sum * 4

Performance test

Perhaps a bit silly but also a fun thing to play with. Pull out hyperfine and compare Python 3.12, Node 20.11, Ruby 3.2, and Bun 1.0.30:


❯ hyperfine --warmup 10 "python3.12 ~/pi.py" "node ~/pi.js" "ruby ~/pi.rb" "bun run ~/pi.js"
Benchmark 1: python3.12 ~/pi.py
  Time (mean ± σ):      53.4 ms ±   7.5 ms    [User: 31.9 ms, System: 12.3 ms]
  Range (min … max):    41.5 ms …  64.8 ms    44 runs

Benchmark 2: node ~/pi.js
  Time (mean ± σ):      57.5 ms ±  10.6 ms    [User: 43.3 ms, System: 11.0 ms]
  Range (min … max):    46.2 ms …  82.6 ms    35 runs

Benchmark 3: ruby ~/pi.rb
  Time (mean ± σ):     242.1 ms ±  11.6 ms    [User: 68.4 ms, System: 37.2 ms]
  Range (min … max):   227.3 ms … 265.3 ms    11 runs

Benchmark 4: bun run ~/pi.js
  Time (mean ± σ):      32.9 ms ±   6.3 ms    [User: 14.1 ms, System: 10.0 ms]
  Range (min … max):    17.1 ms …  41.9 ms    60 runs

Summary
  bun run ~/pi.js ran
    1.62 ± 0.39 times faster than python3.12 ~/pi.py
    1.75 ± 0.46 times faster than node ~/pi.js
    7.35 ± 1.45 times faster than ruby ~/pi.rb

Comparing Pythons

Just because I have a couple of these installed:


❯ hyperfine --warmup 10 "python3.8 ~/pi.py" "python3.9 ~/pi.py" "python3.10 ~/pi.py" "python3.11 ~/pi.py" "python3.12 ~/pi.py"
Benchmark 1: python3.8 ~/pi.py
  Time (mean ± σ):      54.6 ms ±   8.1 ms    [User: 33.0 ms, System: 11.4 ms]
  Range (min … max):    40.0 ms …  69.7 ms    56 runs

Benchmark 2: python3.9 ~/pi.py
  Time (mean ± σ):      54.9 ms ±   8.0 ms    [User: 32.2 ms, System: 12.3 ms]
  Range (min … max):    42.3 ms …  70.1 ms    38 runs

Benchmark 3: python3.10 ~/pi.py
  Time (mean ± σ):      54.7 ms ±   7.5 ms    [User: 33.0 ms, System: 11.8 ms]
  Range (min … max):    42.3 ms …  78.1 ms    44 runs

Benchmark 4: python3.11 ~/pi.py
  Time (mean ± σ):      53.8 ms ±   6.0 ms    [User: 32.7 ms, System: 13.0 ms]
  Range (min … max):    44.8 ms …  70.3 ms    42 runs

Benchmark 5: python3.12 ~/pi.py
  Time (mean ± σ):      53.0 ms ±   6.4 ms    [User: 31.8 ms, System: 12.3 ms]
  Range (min … max):    43.8 ms …  63.5 ms    42 runs

Summary
  python3.12 ~/pi.py ran
    1.02 ± 0.17 times faster than python3.11 ~/pi.py
    1.03 ± 0.20 times faster than python3.8 ~/pi.py
    1.03 ± 0.19 times faster than python3.10 ~/pi.py
    1.04 ± 0.20 times faster than python3.9 ~/pi.py

Notes on porting a Next.js v14 app from Pages to App Router

March 2, 2024
0 comments React, JavaScript

Unfortunately, the app I ported from using the Pages Router to using App Router, is in a private repo. It's a Next.js static site SPA (Single Page App).

It's built with npm run build and then exported so that the out/ directory is the only thing I need to ship to the CDN and it just works. There's a home page and a few dynamic routes whose slugs depend on an SQL query. So the SQL (PostgreSQL) connection, using knex, has to be present when running npm run build.

In no particular order, let's look at some differences

Build times

With caching

After running next build a bunch of times, the rough averages are:

  • Pages Router: 20.5 seconds
  • App Router: 19.5 seconds

Without caching

After running rm -fr .next && next build a bunch of times, the rough averages are:

  • Pages Router: 28.5 seconds
  • App Router: 31 seconds

Note

I have another SPA app that is built with vite and wouter and uses the heavy mantine for the UI library. That SPA app does a LOT more in terms of components and pages etc. That one takes 9 seconds on average.

Static output

If you compare the generated out/_next/static/chunks there's a strange difference.

Pages Router

360.0 KiB [##########################] /pages
268.0 KiB [###################       ]  726-4194baf1eea221e4.js
160.0 KiB [###########               ]  ee8b1517-76391449d3636b6f.js
140.0 KiB [##########                ]  framework-5429a50ba5373c56.js
112.0 KiB [########                  ]  cdfd8999-a1782664caeaab31.js
108.0 KiB [########                  ]  main-930135e47dff83e9.js
 92.0 KiB [######                    ]  polyfills-c67a75d1b6f99dc8.js
 16.0 KiB [#                         ]  502-394e1f5415200700.js
  8.0 KiB [                          ]  0e226fb0-147f1e5268512885.js
  4.0 KiB [                          ]  webpack-1b159842bd89504c.js

In total 1.2 MiB across 15 files.

App Router

428.0 KiB [##########################]  142-94b03af3aa9e6d6b.js
196.0 KiB [############              ]  975-62bfdeceb3fe8dd8.js
184.0 KiB [###########               ]  25-aa44907f6a6c25aa.js
172.0 KiB [##########                ]  fd9d1056-e15083df91b81b75.js
164.0 KiB [##########                ]  ca377847-82e8fe2d92176afa.js
140.0 KiB [########                  ]  framework-aec844d2ccbe7592.js
116.0 KiB [#######                   ]  a6eb9415-a86923c16860379a.js
112.0 KiB [#######                   ]  69-f28d58313be296c0.js
108.0 KiB [######                    ]  main-67e49f9e34a5900f.js
 92.0 KiB [#####                     ]  polyfills-c67a75d1b6f99dc8.js
 44.0 KiB [##                        ] /app
 24.0 KiB [#                         ]  1cc5f7f4-2f067a078d041167.js
 24.0 KiB [#                         ]  250-47a2e67f72854c46.js
  8.0 KiB [                          ] /pages
  4.0 KiB [                          ]  webpack-baa830a732d3dbbf.js
  4.0 KiB [                          ]  main-app-f6b391c808310b44.js

In total 1.7 MiB across 27 files.

Notes

What makes the JS bundle large is most certainly due to using @primer/react, @fullcalendar, and react-chartjs-2.
But why is the difference so large?

Dev start time

The way Next.js works, with npm run dev, is that it starts a server at localhost:3000 and only when you request a URL does it compile something. It's essentially lazy and that's a good thing because in a bigger app, you might have too many different entries so it'd be silly to wait for all of them to compile if you might not use them all.

Pages Router

❯ npm run dev

...

 ✓ Ready in 1125ms
 ○ Compiling / ...
 ✓ Compiled / in 2.9s (495 modules)

App Router

❯ npm run dev

...

 ✓ Ready in 1201ms
 ○ Compiling / ...
 ✓ Compiled / in 3.7s (1023 modules)

Mind you, it almost always says "Ready in 1201ms" or but the other number, like "3.7s" in this example, that seems to fluctuate quite wildly. I don't know why.

Conclusion

Was it worth it? Yes and no.

I've never liked next/router. With App Router you instead use next/navigation which feels much more refined and simple. The old next/router is still there which exposes a useRouter hook which is still used for doing push and replace.

The getStaticPaths and the getStaticProps were not really that terrible in Pages Router.

I think the whole point of App Router is that you can get external data not only in getStaticProps (or getServerSideProps) but you can more freely go and get external data in places like layout.tsx, which means less prop-drilling.

There are some nicer APIs with App Router. And it's the future of Next.js and how Vercel is pushing it forward.

How to avoid a count query in Django if you can

February 14, 2024
1 comment Django, Python

Suppose you have a complex Django QuerySet query that is somewhat costly (in other words slow). And suppose you want to return:

  1. The first N results
  2. A count of the total possible results

So your implementation might be something like this:


def get_results(queryset, fields, size):
    count = queryset.count()
    results = []
    for record in queryset.values(*fields)[:size]
        results.append(record)
    return {"count": count, "results": results}

That'll work. If there are 1,234 rows in your database table that match those specific filters, what you might get back from this is:


>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
1234
>>> len(results["results"])
5

Or, if the filters would only match 3 rows in your database table:


>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
3
>>> len(results["results"])
3

Between your Python application and your database you'll see:

query 1: SELECT COUNT(*) FROM my_database WHERE ...
query 2: SELECT name, age FROM my_database WHERE ... LIMIT 5

The problem with this is that, in the latter case, you had to send two database queries when all you needed was one.
If you knew it would only match a tiny amount of records, you could do this:


def get_results(queryset, fields, size):
-   count = queryset.count()
    results = []
    for record in queryset.values(*fields)[:size]:
        results.append(record)
+   count = len(results)
    return {"count": count, "results": results}

But that is wrong. The count would max out at whatever the size is.

The solution is to try to avoid the potentially unnecessary .count() query.


def get_results(queryset, fields, size):
    count = 0
    results = []
    for i, record in enumerate(queryset.values(*fields)[: size + 1]):
        if i == size:
            # Alas, there are more records than the pagination
            count = queryset.count()
            break
        count = i + 1
        results.append(record)
    return {"count": count, "results": results}

This way, you only incur one database query when there wasn't that much to find, but if there was more than what the pagination called for, you have to incur that extra database query.

Previous page
Next page