note:tue_jun_17_2025

TS

typehero

declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

type Chainable<T = {}> = {
  option<K extends string, V>(key: Exclude<K, keyof T>, value: V): Chainable<Omit<T, K> & {[P in K]: V}>
  get(): T
}

Why K is Inferred, Not T?

TypeScript's inference flows from unknown type parameters based on known argument types:

// When you write:
config.option('database', 'mysql')

// TypeScript thinks:
// - Receiver type: Chainable<{}>, so T = {} (KNOWN)
// - First argument: 'database' (literal string)
//   → Must infer K = 'database' (UNKNOWN → INFERRED)
// - Second argument: 'mysql' (literal string)  
//   → Must infer V = 'mysql' (UNKNOWN → INFERRED)

What If T Were Also a Type Parameter?

If we hypothetically made T also inferrable, it would create ambiguity:

// Hypothetical (problematic) design:
type BadChainable = {
  option<T, K extends string, V>(
    key: Exclude<K, keyof T>, 
    value: V
  ): Chainable<Omit<T, K> & {[P in K]: V}>
}

// How would TypeScript know what T should be?
// There would be no way to infer T from the arguments!

function createStreetLight<C extends string>(colors: C[], defaultColor?: C) {
    // ...
}
createStreetLight(["red", "yellow", "green"], "blue");
//                                            ^^^^^ defaultColor?: "red" | "yellow" | "green" | "blue" | undefined
//                                                  But I don't want blue, because blue is not the color in colors.


// So
function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {
    // ...
}
createStreetLight(["red", "yellow", "green"], "blue");
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

tar -czvf "$(date +'%Y-%m-%d_%H-%M-%S')_config.tar.gz" ./config 
mv *config.tar.gz ./backup
docker compose down && docker compose pull && docker compose up -d

we can use mapped types for creating tuples. It's because tuple properties are indexes.

type A<T extends unknown[]> = {
	[P in keyof T] : T[P]
}

type a =  A<[1,2,3]>
//   ^ type a = [1, 2, 3]

  • note/tue_jun_17_2025.txt
  • Last modified: 2025/06/17 13:00
  • by lingao