Skip to content

Commit

Permalink
docs: keyof
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad committed Jan 4, 2025
1 parent 9421778 commit d2ce054
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 29 deletions.
36 changes: 20 additions & 16 deletions ark/docs/components/SyntaxTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const syntaxKinds = [
"string",
"fluent",
"tuple",
"spread"
"args"
// don't infer as readonly since Fumadocs (incorrectly) doesn't support that
] as const satisfies string[]

Expand All @@ -15,21 +15,25 @@ export type SyntaxKind = (typeof syntaxKinds)[number]
export const SyntaxTabs: React.FC<omit<TabsProps, "items">> = ({
children,
...rest
}) => (
<Tabs
{...rest}
// only include the tabs that were actually used
items={syntaxKinds.filter(kind =>
Children.toArray(children).some(
child =>
isValidElement(child) &&
(child.props as SyntaxTabProps | undefined)?.[kind]
)
)}
>
{children}
</Tabs>
)
}) => {
const usedKinds = Children.toArray(children).flatMap(child => {
if (!isValidElement(child)) return []
if (!child.props) return []

const props = child.props as SyntaxTabProps

const matchingKind = syntaxKinds.find(k => props[k])
if (!matchingKind) return []

return matchingKind
})

return (
<Tabs {...rest} items={usedKinds}>
{children}
</Tabs>
)
}

type DiscriminatedSyntaxKindProps = unionToPropwiseXor<
{
Expand Down
14 changes: 14 additions & 0 deletions ark/docs/components/snippets/contentsById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
betterErrors:
'import { type, type ArkErrors } from "arktype"\n\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"versions?": "(number | string)[]"\n})\n\ninterface RuntimeErrors extends ArkErrors {\n\t/**platform must be "android" or "ios" (was "enigma")\nversions[2] must be a number or a string (was bigint)*/\n\tsummary: string\n}\n\nconst narrowMessage = (e: ArkErrors): e is RuntimeErrors => true\n\n// ---cut---\nconst out = user({\n\tname: "Alan Turing",\n\tplatform: "enigma",\n\tversions: [0, "1", 0n]\n})\n\nif (out instanceof type.errors) {\n\t// ---cut-start---\n\tif (!narrowMessage(out)) throw new Error()\n\t// ---cut-end---\n\t// hover summary to see validation errors\n\tconsole.error(out.summary)\n}\n',
clarityAndConcision:
'// @errors: 2322\nimport { type } from "arktype"\n// this file is written in JS so that it can include a syntax error\n// without creating a type error while still displaying the error in twoslash\n// ---cut---\n// hover me\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"versions?": "number | string)[]"\n})\n',
deepIntrospectability:
'import { type } from "arktype"\n\nconst user = type({\n\tname: "string",\n\tdevice: {\n\t\tplatform: "\'android\' | \'ios\'",\n\t\t"version?": "number | string"\n\t}\n})\n\n// ---cut---\nuser.extends("object") // true\nuser.extends("string") // false\n// true (string is narrower than unknown)\nuser.extends({\n\tname: "unknown"\n})\n// false (string is wider than "Alan")\nuser.extends({\n\tname: "\'Alan\'"\n})\n',
intrinsicOptimization:
'import { type } from "arktype"\n// prettier-ignore\n// ---cut---\n// all unions are optimally discriminated\n// even if multiple/nested paths are needed\nconst account = type({\n\tkind: "\'admin\'",\n\t"powers?": "string[]"\n}).or({\n\tkind: "\'superadmin\'",\n\t"superpowers?": "string[]"\n}).or({\n\tkind: "\'pleb\'"\n})\n',
unparalleledDx:
'// @noErrors\nimport { type } from "arktype"\n// prettier-ignore\n// ---cut---\nconst user = type({\n\tname: "string",\n\tplatform: "\'android\' | \'ios\'",\n\t"version?": "number | s"\n\t// ^|\n})\n',
nestedTypeInScopeError:
'// @errors: 2322\nimport { scope } from "arktype"\n// ---cut---\nconst myScope = scope({\n\tid: "string#id",\n\tuser: type({\n\t\tname: "string",\n\t\tid: "id"\n\t})\n})\n'
}
16 changes: 8 additions & 8 deletions ark/docs/content/docs/expressions/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const foobarObject = type([

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
// an object requiring both foo and bar
Expand Down Expand Up @@ -106,7 +106,7 @@ const unions = type({

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const unions = type({
Expand Down Expand Up @@ -211,7 +211,7 @@ const out = form({

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const form = type(
Expand Down Expand Up @@ -273,7 +273,7 @@ const arkString = type([

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
// hover to see how the predicate is propagated to the outer `Type`
Expand Down Expand Up @@ -312,7 +312,7 @@ const trimStringStart = type(["string", "=>", str => str.trimStart()])

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
// hover to see how morphs are represented at a type-level
Expand Down Expand Up @@ -349,7 +349,7 @@ const exactValue = type(["===", mySymbol])

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const mySymbol = Symbol()
Expand Down Expand Up @@ -387,7 +387,7 @@ const exactValueFromSet = type(["===", 1337, true, mySymbol])

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const mySymbol = Symbol()
Expand Down Expand Up @@ -440,7 +440,7 @@ const specialNumber = type(["number", "@", "a special number"])

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
// this validator's error message will now start with "must be a special string"
Expand Down
143 changes: 138 additions & 5 deletions ark/docs/content/docs/objects/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ type Result = typeof types.result.infer
</SyntaxTab>
<SyntaxTab fluent>
<SyntaxTab fluent>
```ts
const zildjian = Symbol()
Expand All @@ -334,7 +334,140 @@ const chainedResult = base.merge({

### keyof [#properties-keyof]

🚧 Coming soon ™️🚧
Like in TypeScript, the `keyof` operator extracts the keys of an object as a union:

<SyntaxTabs>
<SyntaxTab fluent>

```ts
const usedCar = type({
originallyPurchased: "string.date",
remainingWheels: "number"
})

const usedCarKey = usedCar.keyof()

type UsedCarKey = typeof usedCarKey.infer
```
</SyntaxTab>
<SyntaxTab string>
```ts
const types = type.module({
usedCar: {
originallyPurchased: "string.date",
remainingWheels: "number"
},
usedCarKey: "keyof usedCar"
})

type UsedCarKey = typeof types.usedCarKey.infer
```
</SyntaxTab>
<SyntaxTab tuple>
```ts
const usedCar = type({
originallyPurchased: "string.date",
remainingWheels: "number"
})

const usedCarKey = type(["keyof", usedCar])

type UsedCarKey = typeof usedCarKey.infer
```
</SyntaxTab>
<SyntaxTab args>
```ts
const usedCar = type({
originallyPurchased: "string.date",
remainingWheels: "number"
})

const usedCarKey = type("keyof", usedCar)

type UsedCarKey = typeof usedCarKey.infer
```
</SyntaxTab>
</SyntaxTabs>
Also like in TypeScript, if an object includes an index signature like `[string]` alongside named properties, the union from `keyof` will reduce to `string`:
```ts
const recordWithSpecialKeys = type({
"[string]": "unknown",
verySpecialKey: "0 < number <= 3.14159",
moderatelySpecialKey: "-9.51413 <= number < 0"
})

// in a union with the `string` index signature, string literals
// "verySpecialKey" and "moderatelySpecialKey" are redundant and will be pruned
const key = recordWithSpecialKeys.keyof()

// key is identical to the base `string` Type
console.log(key.equals("string"))
```

<Callout type="warn" title="ArkType's `keyof` will never include `number`">
Though TypeScript's `keyof` operator can yield a `number`, the concept of
numeric keys does not exist in JavaScript at runtime. This leads to confusing
and inconsistent behavior. In ArkType, `keyof` will always return a `string`
or `symbol` in accordance with the construction of a JavaScript object.

<details>
<summary>Learn more about our motivation for diverging from TypeScript on this issue</summary>

In JavaScript, you can use a number literal to define a key, but the constructed value has no way to represent a numeric key, so it is coerced to a string.

```ts
const numberLiteralObj = {
4: true,
5: true
}

const stringLiteralObj = {
"4": true,
"5": true
}

// numberLiteralObj and stringLiteralObj are indistinguishable at this point
Object.keys(numberLiteralObj) // ["4", "5"]
Object.keys(stringLiteralObj) // ["4", "5"]
```

For a set-based type system to be correct, any two types representing the same set of underlying values must share a single representation. TypeScript's decision to have distinct numeric and string representations for the same underlying key has led to some if its most confusing inference pitfalls:

```ts
type Thing1 = {
[x: string]: unknown
}

// Thing2 is apparently identical to Thing1
type Thing2 = Record<string, unknown>

// and yet...
type Key1 = keyof Thing1
// ^?

type Key2 = keyof Thing2
// ^?
```
This sort of inconsistency is inevitable for a type system that has to reconcile multiple representations
for identical sets of underlying values. Therefore, numeric keys are one of a handful of cases where ArkType intentionally diverges from TypeScript. ArkType will never return a `number` from `keyof`. Keys will always be normalized to a `string` or `symbol`, the two distinct property types that can be uniquely attached to a JavaScript object.
</details>
</Callout>
### get [#properties-get]
Expand Down Expand Up @@ -373,7 +506,7 @@ const arrays = type({

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const arrays = type({
Expand Down Expand Up @@ -535,7 +668,7 @@ const myTuple = type([

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
const myTuple = type([
Expand Down Expand Up @@ -715,7 +848,7 @@ const instances = type({

</SyntaxTab>

<SyntaxTab spread>
<SyntaxTab args>

```ts
class MyClass {}
Expand Down
11 changes: 11 additions & 0 deletions ark/repo/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ const customEven = type("number % 2", "@", {

// custom message custom problem custom expected a multiple of 2 custom actual 3
customEven(3)

type Thing1 = {
[x: string]: unknown
}

// Thing2 is apparently identical to Thing1, and yet...
type Thing2 = Record<string, unknown>

type A = keyof Thing1 // number | string

type B = keyof Thing2 // string
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@
"allowedVersions": {
"eslint": "*"
}
},
"overrides": {
"@shikijs/types": "1.26.1"
}
},
"packageManager": "pnpm@9.14.4"
Expand Down

0 comments on commit d2ce054

Please sign in to comment.