What is Hackers' Pub?

Hackers' Pub is a place for software engineers to share their knowledge and experience with each other. It's also an ActivityPub-enabled social network, so you can follow your favorite hackers in the fediverse and get their latest posts in your feed.

0
0
0
0
0
2
0
0

서울 올라왔을때 여러가지로 충격을 받았는데 그중 하나가 최저임금을 지키는 알바가 더 많고 최저임금 안지키는게 이상한 취급을 받는것이었음 대구는 어디 시골도 아니고 대도시 한복판에서도 알바하면(특히 편의점 알바는 거의 무조건) 한 열에 여덟정도는 최저임금 절대 안 줌 당연히 야간 등 각종 수당도 절대 안 줌

0

아침에 마신 두유팩, 버릴 땐 종이류? 일반쓰레기?···정답은 ‘둘 다 아닙니다’

n.news.naver.com/article/032/0

"폴리에틸렌 필름 덧대져 있어 일반 재활용 불가
‘종이팩 분리함’ 넣어야 하지만 마련된 곳 적어
최고급 펄프 소재, 시스템 미비로 재활용 19% 불과
기후에너지부, 올해 전국 아파트 배출함 설치 예고"

배출함 설치해봐야....잘 안될듯.
너무 복잡해지면 실천이 잘 안되기도 하고, 저게 좀 이익이 되야 업체들이 많이 참여하는데 그것도 아니고.

0

【BUILT-IN PRO 電器專門店特約】日本「千年一遇美少女」橋本環奈在2026年元旦迎來強勁開局,於富士電視台復活播映的經典益智節目《猜謎$百萬富翁》(クイズ$ミリオネア)中連過12題,最終成功奪得1000萬日圓(約50萬港元)獎金,成為節目史上首位滿額得獎者,被網民大讚「運氣與實力兼備」。

節目相隔13年再度推出,由人氣組合「嵐」成員二宮和也擔任主持,並邀請菊池風磨、ano、室剛等藝人參與。橋本環奈作為首位挑戰者表現亮眼,面對層層難題仍冷靜應戰。最驚險一刻出現在最後一題——在1897年前、東京成為人口第一城市之前,哪一個縣市曾為日本人口最多?選項包括新潟縣、愛知縣、廣島縣及熊本縣。

她一度猶豫,更啟動「電話求助」向合作拍攝《不良醫生》的演員大谷亮平及許豐凡請教,惟二人亦未能確定答案。最終她憑直覺選擇「新潟縣」,結果正確,全場即時歡呼,氣氛推向高潮。當主持人二宮和也遞上印有1000萬日圓的支票時,橋本環奈一度呆住難以置信,二宮更大讚她判斷力驚人,形容這種好運「相當罕見」。

節目片段播出後即成熱話,網民紛紛留言:「真的女神級運勢」、「娛樂圈與知識都能全勝,太勵志」。

May be an image of text
0

Designing type-safe sync/async mode support in TypeScript

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

I recently added sync/async mode support to Optique, a type-safe CLI parser for TypeScript. It turned out to be one of the trickier features I've implemented—the object() combinator alone needed to compute a combined mode from all its child parsers, and TypeScript's inference kept hitting edge cases.

What is Optique?

Optique is a type-safe, combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative. Instead of decorators or builder patterns, you compose small parsers into larger ones using combinators, and TypeScript infers the result types.

Here's a quick taste:

import { object } from "@optique/core/constructs";
import { argument, option } from "@optique/core/primitives";
import { string, integer } from "@optique/core/valueparser";
import { run } from "@optique/run";

const cli = object({
  name: argument(string()),
  count: option("-n", "--count", integer()),
});

// TypeScript infers: { name: string; count: number | undefined }
const result = run(cli);  // sync by default

The type inference works through arbitrarily deep compositions—in most cases, you don't need explicit type annotations.

How it started

Lucas Garron (@lgarron) opened an issue requesting async support for shell completions. He wanted to provide Tab-completion suggestions by running shell commands like git for-each-ref to list branches and tags.

// Lucas's example: fetching Git branches and tags in parallel
const [branches, tags] = await Promise.all([
  $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(),
  $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(),
]);

At first, I didn't like the idea. Optique's entire API was synchronous, which made it simpler to reason about and avoided the “async infection” problem where one async function forces everything upstream to become async. I argued that shell completion should be near-instantaneous, and if you need async data, you should cache it at startup.

But Lucas pushed back. The filesystem is a database, and many useful completions inherently require async work—Git refs change constantly, and pre-caching everything at startup doesn't scale for large repos. Fair point.

What I needed to solve

So, how do you support both sync and async execution modes in a composable parser library while maintaining type safety?

The key requirements were:

  • parse() returns T or Promise<T>
  • complete() returns T or Promise<T>
  • suggest() returns Iterable<T> or AsyncIterable<T>
  • When combining parsers, if any parser is async, the combined result must be async
  • Existing sync code should continue to work unchanged

The fourth requirement is the tricky one. Consider this:

const syncParser = flag("--verbose");
const asyncParser = option("--branch", asyncValueParser);

// What's the type of this?
const combined = object({ verbose: syncParser, branch: asyncParser });

The combined parser should be async because one of its fields is async. This means we need type-level logic to compute the combined mode.

Five design options

I explored five different approaches, each with its own trade-offs.

Option A: conditional types with mode parameter

Add a mode type parameter to Parser and use conditional types:

type Mode = "sync" | "async";

type ModeValue<M extends Mode, T> = M extends "async" ? Promise<T> : T;

interface Parser<M extends Mode, TValue, TState> {
  parse(context: ParserContext<TState>): ModeValue<M, ParserResult<TState>>;
  // ...
}

The challenge is computing combined modes:

type CombineModes<T extends Record<string, Parser<any, any, any>>> =
  T[keyof T] extends Parser<infer M, any, any>
    ? M extends "async" ? "async" : "sync"
    : never;

Option B: mode parameter with default value

A variant of Option A, but place the mode parameter first with a default of "sync":

interface Parser<M extends Mode = "sync", TValue, TState> {
  readonly $mode: M;
  // ...
}

The default value maintains backward compatibility—existing user code keeps working without changes.

Option C: separate interfaces

Define completely separate Parser and AsyncParser interfaces with explicit conversion:

interface Parser<TValue, TState> { /* sync methods */ }
interface AsyncParser<TValue, TState> { /* async methods */ }

function toAsync<T, S>(parser: Parser<T, S>): AsyncParser<T, S>;

Simpler to understand, but requires code duplication and explicit conversions.

Option D: union return types for suggest() only

The minimal approach. Only allow suggest() to be async:

interface Parser<TValue, TState> {
  parse(context: ParserContext<TState>): ParserResult<TState>;  // always sync
  suggest(context: ParserContext<TState>, prefix: string):
    Iterable<Suggestion> | AsyncIterable<Suggestion>;  // can be either
}

This addresses the original use case but doesn't help if async parse() is ever needed.

Option E: fp-ts style HKT simulation

Use the technique from fp-ts to simulate Higher-Kinded Types:

interface URItoKind<A> {
  Identity: A;
  Promise: Promise<A>;
}

type Kind<F extends keyof URItoKind<any>, A> = URItoKind<A>[F];

interface Parser<F extends keyof URItoKind<any>, TValue, TState> {
  parse(context: ParserContext<TState>): Kind<F, ParserResult<TState>>;
}

The most flexible approach, but with a steep learning curve.

Testing the idea

Rather than commit to an approach based on theoretical analysis, I created a prototype to test how well TypeScript handles the type inference in practice. I published my findings in the GitHub issue:

Both approaches correctly handle the “any async → all async” rule at the type level. (…) Complex conditional types like ModeValue<CombineParserModes<T>, ParserResult<TState>> sometimes require explicit type casting in the implementation. This only affects library internals. The user-facing API remains clean.

The prototype validated that Option B (explicit mode parameter with default) would work. I chose it for these reasons:

  • Backward compatible: The default "sync" keeps existing code working
  • Explicit: The mode is visible in both types and runtime (via a $mode property)
  • Debuggable: Easy to inspect the current mode at runtime
  • Better IDE support: Type information is more predictable

How CombineModes works

The CombineModes type computes whether a combined parser should be sync or async:

type CombineModes<T extends readonly Mode[]> = "async" extends T[number]
  ? "async"
  : "sync";

This type checks if "async" is present anywhere in the tuple of modes. If so, the result is "async"; otherwise, it's "sync".

For combinators like object(), I needed to extract modes from parser objects and combine them:

// Extract the mode from a single parser
type ParserMode<T> = T extends Parser<infer M, unknown, unknown> ? M : never;

// Combine modes from all values in a record of parsers
type CombineObjectModes<T extends Record<string, Parser<Mode, unknown, unknown>>> =
  CombineModes<{ [K in keyof T]: ParserMode<T[K]> }[keyof T][]>;

Runtime implementation

The type system handles compile-time safety, but the implementation also needs runtime logic. Each parser has a $mode property that indicates its execution mode:

const syncParser = option("-n", "--name", string());
console.log(syncParser.$mode);  // "sync"

const asyncParser = option("-b", "--branch", asyncValueParser);
console.log(asyncParser.$mode);  // "async"

Combinators compute their mode at construction time:

function object<T extends Record<string, Parser<Mode, unknown, unknown>>>(
  parsers: T
): Parser<CombineObjectModes<T>, ObjectValue<T>, ObjectState<T>> {
  const parserKeys = Reflect.ownKeys(parsers);
  const combinedMode: Mode = parserKeys.some(
    (k) => parsers[k as keyof T].$mode === "async"
  ) ? "async" : "sync";

  // ... implementation
}

Refining the API

Lucas suggested an important refinement during our discussion. Instead of having run() automatically choose between sync and async based on the parser mode, he proposed separate functions:

Perhaps run(…) could be automatic, and runSync(…) and runAsync(…) could enforce that the inferred type matches what is expected.

So we ended up with:

  • run(): automatic based on parser mode
  • runSync(): enforces sync mode at compile time
  • runAsync(): enforces async mode at compile time
// Automatic: returns T for sync parsers, Promise<T> for async
const result1 = run(syncParser);  // string
const result2 = run(asyncParser);  // Promise<string>

// Explicit: compile-time enforcement
const result3 = runSync(syncParser);  // string
const result4 = runAsync(asyncParser);  // Promise<string>

// Compile error: can't use runSync with async parser
const result5 = runSync(asyncParser);  // Type error!

I applied the same pattern to parse()/parseSync()/parseAsync() and suggest()/suggestSync()/suggestAsync() in the facade functions.

Creating async value parsers

With the new API, creating an async value parser for Git branches looks like this:

import type { Suggestion } from "@optique/core/parser";
import type { ValueParser, ValueParserResult } from "@optique/core/valueparser";

function gitRef(): ValueParser<"async", string> {
  return {
    $mode: "async",
    metavar: "REF",
    parse(input: string): Promise<ValueParserResult<string>> {
      return Promise.resolve({ success: true, value: input });
    },
    format(value: string): string {
      return value;
    },
    async *suggest(prefix: string): AsyncIterable<Suggestion> {
      const { $ } = await import("bun");
      const [branches, tags] = await Promise.all([
        $`git for-each-ref --format='%(refname:short)' refs/heads/`.text(),
        $`git for-each-ref --format='%(refname:short)' refs/tags/`.text(),
      ]);
      for (const ref of [...branches.split("\n"), ...tags.split("\n")]) {
        const trimmed = ref.trim();
        if (trimmed && trimmed.startsWith(prefix)) {
          yield { kind: "literal", text: trimmed };
        }
      }
    },
  };
}

Notice that parse() returns Promise.resolve() even though it's synchronous. This is because the ValueParser<"async", T> type requires all methods to use async signatures. Lucas pointed out this is a minor ergonomic issue. If only suggest() needs to be async, you still have to wrap parse() in a Promise.

I considered per-method mode granularity (e.g., ValueParser<ParseMode, SuggestMode, T>), but the implementation complexity would multiply substantially. For now, the workaround is simple enough:

// Option 1: Use Promise.resolve()
parse(input) {
  return Promise.resolve({ success: true, value: input });
}

// Option 2: Mark as async and suppress the linter
// biome-ignore lint/suspicious/useAwait: sync implementation in async ValueParser
async parse(input) {
  return { success: true, value: input };
}

What it cost

Supporting dual modes added significant complexity to Optique's internals. Every combinator needed updates:

  • Type signatures grew more complex with mode parameters
  • Mode propagation logic had to be added to every combinator
  • Dual implementations were needed for sync and async code paths
  • Type casts were sometimes necessary in the implementation to satisfy TypeScript

For example, the object() combinator went from around 100 lines to around 250 lines. The internal implementation uses conditional logic based on the combined mode:

if (combinedMode === "async") {
  return {
    $mode: "async" as M,
    // ... async implementation with Promise chains
    async parse(context) {
      // ... await each field's parse result
    },
  };
} else {
  return {
    $mode: "sync" as M,
    // ... sync implementation
    parse(context) {
      // ... directly call each field's parse
    },
  };
}

This duplication is the cost of supporting both modes without runtime overhead for sync-only use cases.

Lessons learned

Listen to users, but validate with prototypes

My initial instinct was to resist async support. Lucas's persistence and concrete examples changed my mind, but I validated the approach with a prototype before committing. The prototype revealed practical issues (like TypeScript inference limits) that pure design analysis would have missed.

Backward compatibility is worth the complexity

Making "sync" the default mode meant existing code continued to work unchanged. This was a deliberate choice. Breaking changes should require user action, not break silently.

Unified mode vs per-method granularity

I chose unified mode (all methods share the same sync/async mode) over per-method granularity. This means users occasionally write Promise.resolve() for methods that don't actually need async, but the alternative was multiplicative complexity in the type system.

Designing in public

The entire design process happened in a public GitHub issue. Lucas, Giuseppe, and others contributed ideas that shaped the final API. The runSync()/runAsync() distinction came directly from Lucas's feedback.

Conclusion

This was one of the more challenging features I've implemented in Optique. TypeScript's type system is powerful enough to encode the “any async means all async” rule at compile time, but getting there required careful design work and prototyping.

What made it work: conditional types like ModeValue<M, T> can bridge the gap between sync and async worlds. You pay for it with implementation complexity, but the user-facing API stays clean and type-safe.

Optique 0.9.0 with async support is currently in pre-release testing. If you'd like to try it, check out PR #70 or install the pre-release:

npm  add       @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212
deno add --jsr @optique/core@0.9.0-dev.212 @optique/run@0.9.0-dev.212

Feedback is welcome!

Read more →
4
1
0
0
0
0
0

[RP❤️] 쭈글쭈글 공격을 할 수 있는 2일 마감 커미션! 현재 작업 속도가 빨라 마감 기한을 줄였어요 😚 원트의 RP 추첨 무료 작업은 이 글도 합산하여 적용됩니다 (수동 추첨으로 진행해요) 자세한 공지는 크레페에 있습니다. 늘 감사합니다 크레페: kre.pe/B0F4

RE: https://bsky.app/profile/did:plc:6be4vrwlluukyvnhvef2icff/post/3lkxnoxvttc23

0
0
0
0
0
0
1
0

아침에 마신 두유팩, 버릴 땐 종이류? 일반쓰레기?···정답은 ‘둘 다 아닙니다’

n.news.naver.com/article/032/0

"폴리에틸렌 필름 덧대져 있어 일반 재활용 불가
‘종이팩 분리함’ 넣어야 하지만 마련된 곳 적어
최고급 펄프 소재, 시스템 미비로 재활용 19% 불과
기후에너지부, 올해 전국 아파트 배출함 설치 예고"

배출함 설치해봐야....잘 안될듯.
너무 복잡해지면 실천이 잘 안되기도 하고, 저게 좀 이익이 되야 업체들이 많이 참여하는데 그것도 아니고.

0
0
0
0
0
4
0
1
0

For the new year's todo-list I have one thing to do. Start my own instance, so that I can interact with others with my name and project. I have already "elettrona @ plusbrothers.net" on my main project's site, based on platform. But I don't manage to reply through mastodon clients as it returns an error using Enable Mastodon App plugin.
But if I have my own instance, with long text writing enabled maybe, I can decide. Reading from here (and MastodonUno) keeping noise there, while interacting with the other one.

0
1
1
0
0
0

I usually head to bed before my husband so I make him tuck me in like a burrito but then if I can't fall asleep I shout "microwave burrito explosion!" and then roll off the side of the bed while I wiggle around the carpeting leaving a trail of molten cheese in my wake.

0
1

我媽真是不用情勒不會溝通耶

在公司在忙,差一點點就好了
他忘記帶鑰匙,偏要用電話打來不用打字
這邊接電話基本上都要出去門外
連打幾次最後連伴侶也找上,就只是為了問我拿鑰匙

然後就在群組說什麼老來可憐沒人理之類的

0
1
0
0
0
0
1
0

【BUILT-IN PRO 電器專門店特約】日本「千年一遇美少女」橋本環奈在2026年元旦迎來強勁開局,於富士電視台復活播映的經典益智節目《猜謎$百萬富翁》(クイズ$ミリオネア)中連過12題,最終成功奪得1000萬日圓(約50萬港元)獎金,成為節目史上首位滿額得獎者,被網民大讚「運氣與實力兼備」。

節目相隔13年再度推出,由人氣組合「嵐」成員二宮和也擔任主持,並邀請菊池風磨、ano、室剛等藝人參與。橋本環奈作為首位挑戰者表現亮眼,面對層層難題仍冷靜應戰。最驚險一刻出現在最後一題——在1897年前、東京成為人口第一城市之前,哪一個縣市曾為日本人口最多?選項包括新潟縣、愛知縣、廣島縣及熊本縣。

她一度猶豫,更啟動「電話求助」向合作拍攝《不良醫生》的演員大谷亮平及許豐凡請教,惟二人亦未能確定答案。最終她憑直覺選擇「新潟縣」,結果正確,全場即時歡呼,氣氛推向高潮。當主持人二宮和也遞上印有1000萬日圓的支票時,橋本環奈一度呆住難以置信,二宮更大讚她判斷力驚人,形容這種好運「相當罕見」。

節目片段播出後即成熱話,網民紛紛留言:「真的女神級運勢」、「娛樂圈與知識都能全勝,太勵志」。

May be an image of text
0
0
1