[Typescript challenge] 20. Medium - Chainable Options
Chainable options are commonly used in Javascript. But when we switch to TypeScript, can you properly type it?
In this challenge, you need to type an object or a class - whatever you like - to provide two function option(key, value)
and get()
. In option
, you can extend the current config type by the given key and value. We should about to access the final result via get
.
For example
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
}
}
You don't need to write any js/ts logic to handle the problem - just in type level.
You can assume that key
only accepts string
and the value
can be anything - just leave it as-is. Same key
won't be passed twice.
Solution:
type Chainable<T = {}> = {
option<K extends string, V>(key: K extends keyof T ? (V extends T[K] ? never: K): K, value: V): Chainable<Omit<T, K> & {[P in K]: V}>,
get(): T
}
Test case:
/* _____________ Test Cases _____________ */
import type { Alike, Expect } from '@type-challenges/utils'
type x = 'a' extends 'a' ? true: false
declare const a: Chainable
const result1 = a
.option('foo', 123)
.option('bar', { value: 'Hello World' })
.option('name', 'type-challenges')
.get()
// If key is the same, and value is the same type
// then should expect error
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
// If key is the same, but value is different type
// then should override the value with the same key
const result3 = a
.option('name', 'another name')
.option('name', 123)
.get()
type cases = [
Expect<Alike<typeof result1, Expected1>>,
Expect<Alike<typeof result2, Expected2>>,
Expect<Alike<typeof result3, Expected3>>,
]
type Expected1 = {
foo: number
bar: {
value: string
}
name: string
}
type Expected2 = {
name: string
}
type Expected3 = {
name: number
}
Notice Test 2:
// If key is the same, and value is the same type
// then should expect error
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
This means, we need to control key's type:
key: K extends keyof T ? (V extends T[K] ? never: K): K
if K is already inside T, then check V is the same type of T[K], if they are the same type, should be never, otherwise keep K.// If key is the same, but value is different type
// then should override the value with the same key
const result3 = a
.option('name', 'another name')
.option('name', 123)
.get()
type Expected3 = {
name: number
}
The finial output is number type. Which means should override previous type.
Chainable<Omit<T, K> & {[P in K]: V}>
If you don't omit previous type, then it should be
{
name: string
} & {
name: number
}