Skip to content

Commit

Permalink
feat(xor): support xoring more than two types (up to 200!)
Browse files Browse the repository at this point in the history
  • Loading branch information
maninak committed Sep 14, 2023
1 parent 8eeb3e3 commit 3d7ffa6
Show file tree
Hide file tree
Showing 32 changed files with 319 additions and 64 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ tsconfig.json
.gitignore
.npmrc
.travis.yml
.editorconfig
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const test: WeatherForecast = {

### XORing more than two types

If you want to create a type as the product of the logical XOR operation between multiple types (more than two), then nest the generic params.
If you want to create a type as the product of the logical XOR operation between multiple types (more than two and up to even 100), then just pass them as additional comma-separated generic params.

```typescript
import type { XOR } from 'ts-xor'
Expand All @@ -145,7 +145,7 @@ interface A { a: string }
interface B { b: string }
interface C { c: string }

let test: XOR<A, XOR<B, C>>
let test: XOR<A, B, C>

test = { a: '' } // OK
test = { b: '' } // OK
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
},
"sideEffects": false,
"scripts": {
"codegen": "node ./src/xorFactory.js > ./src/types/xor.ts",
"prebuild": "npm run codegen",
"build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean",
"pretest": "npm run codegen",
"test": "npm run test:smoke && npm run test:unit && npm run test:package",
"test:smoke": "tsc -p . --noEmit",
"test:unit": "sh scripts/run-tests.sh",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './types/xor.js'
export type * from './types/xor.js'
4 changes: 4 additions & 0 deletions src/types/evalIfNotUnknown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Skip evaluating `U` if `T` is `unknown`.
*/
export type EvalIfNotUnknown<T, U> = unknown extends T ? never : U;
30 changes: 17 additions & 13 deletions src/types/xor.ts

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions src/xorFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const ts = require('typescript')

const xorParamCount = 200
const countOfUniqueLetters = 20
/**
* Contains ['A', 'B', ..., <countOfUniqueLetters'th_letter_used_in_Array_constructor>]
*/
const uniqueLetters = [...Array(countOfUniqueLetters).keys()]
.map(i => String.fromCharCode(i + 65))
const allParamNames = getUniqueSymbolPermutationsGivenPool(uniqueLetters, xorParamCount)
const [,, ...paramNamesExcludingANorB] = allParamNames

function createXor() {
const modifiers = [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)]
const name = ts.factory.createIdentifier('XOR')
const typeParams = createXorParams()
const type = createXorType()

return ts.factory.createTypeAliasDeclaration(modifiers, name, typeParams, type)
}

function createXorParams() {
const xorParams = [
ts.factory.createTypeParameterDeclaration(undefined, ts.factory.createIdentifier('A')),
ts.factory.createTypeParameterDeclaration(undefined, ts.factory.createIdentifier('B')),
...paramNamesExcludingANorB.map((letter) => ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier(letter),
undefined,
ts.factory.createTypeReferenceNode('unknown')
))
]

return xorParams
}

function createXorType() {
const unionOfWithouts = ts.factory.createUnionTypeNode([
createWithoutLettersIntersectingLetter(
allParamNames.filter((letterToExclude) => letterToExclude !== 'A'),
'A',
),
createWithoutLettersIntersectingLetter(
allParamNames.filter((letterToExclude) => letterToExclude !== 'B'),
'B',
),
...paramNamesExcludingANorB.map(
(letter) => ts.factory.createTypeReferenceNode(
'EvalIfNotUnknown',
[
ts.factory.createTypeReferenceNode(letter),
createWithoutLettersIntersectingLetter(
allParamNames.filter((letterToExclude) => letterToExclude !== letter),
letter,
),
]
)
)
])

const type = ts.factory.createTypeReferenceNode('Prettify', [unionOfWithouts])

return type
}

/**
* @param {string[]} lettersExcludingLetter
* @param {string} excludedLetter
*/
function createWithoutLettersIntersectingLetter(lettersExcludingLetter, excludedLetter) {
const withoutLettersIntersectingLetter = ts.factory.createIntersectionTypeNode([
createWithout(lettersExcludingLetter, excludedLetter),
ts.factory.createTypeReferenceNode(excludedLetter)
])

return withoutLettersIntersectingLetter
}

/**
* @param {string[]} lettersExcludingLetter
* @param {string} excludedLetter
*/
function createWithout(lettersExcludingLetter, excludedLetter) {
const type = ts.factory.createTypeReferenceNode('Without', [
ts.factory.createIntersectionTypeNode(
lettersExcludingLetter.map((letter) => ts.factory.createTypeReferenceNode(letter))
),
ts.factory.createTypeReferenceNode(excludedLetter)
])

return type
}

/**
* Takes a `symbolPool` and uses them solo and then matches them in pairs until
* the provided count of unique symbols is reached.
* If all possible pairs with the available symbols are already created and the
* `countPermsToGenerate` is still not reached, then triplets will start to be generated,
* then quadruplets, etc.
*
* @example
* ```ts
* getUniqueSymbolPermutationsGivenPool(['A', 'B'], 8)
* // ['A', 'B', 'AA', 'AB', 'BA', 'BB', 'AAA', 'AAB']
* ```
*
* @param {string[]} symbolPool
* @param {number} countPermsToGenerate
*/
function getUniqueSymbolPermutationsGivenPool(symbolPool, countPermsToGenerate) {
const generateItem = (index) => {
if (index < 0) {
return ''
}
const remainder = index % 20
return generateItem(Math.floor(index / 20) - 1) + symbolPool[remainder]
}

const result = Array.from({ length: countPermsToGenerate }, (_, i) => generateItem(i))

return result
}

const tempFile = ts.createSourceFile(
'temp.ts',
'',
ts.ScriptTarget.ESNext,
false, ts.ScriptKind.TS,
)
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
omitTrailingSemicolon: true,
})

const xorTsFileContents = `
import type { EvalIfNotUnknown } from './evalIfNotUnknown.js'
import type { Prettify } from './prettify.js'
import type { Without } from './without.js'
${
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createJSDocComment(
`Restrict using either exclusively the keys of \`T\` or \
exclusively the keys of \`U\`.\n\n\
No unique keys of \`T\` can be used simultaneously with \
any unique keys of \`U\`.\n\n@example\n\
\`\`\`ts\nconst myVar: XOR<{ data: object }, { error: object }>\n\`\`\`\n\n\
Supports from 2 up to ${xorParamCount} generic parameters.\n\n\
More: https://github.com/maninak/ts-xor/tree/master#description\n`
),
tempFile,
)
}
${
printer.printNode(ts.EmitHint.Unspecified, createXor(), tempFile)
}`

console.log(xorTsFileContents)
9 changes: 2 additions & 7 deletions test/control-std-union-without-xor/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
interface A {
a: string
}

interface B {
b: string
}
interface A { a: string }
interface B { b: string }

export type A_OR_B = A | B
6 changes: 6 additions & 0 deletions test/four-xored-types/has-keys-of-A-and-C.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

// @ts-expect-error
const test: XOR_A_B_C_D = { a: '', c: '' }
// @ts-expect-error
const testNested: XOR_A_B_C_D_Nested = { a: '', c: '' }
6 changes: 6 additions & 0 deletions test/four-xored-types/has-keys-of-A-and-D.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

// @ts-expect-error
const test: XOR_A_B_C_D = { a: '', d: '' }
// @ts-expect-error
const testNested: XOR_A_B_C_D_Nested = { a: '', d: '' }
4 changes: 4 additions & 0 deletions test/four-xored-types/has-keys-of-A.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

const test: XOR_A_B_C_D = { a: '' } // OK
const testNested: XOR_A_B_C_D_Nested = { a: '' } // OK
4 changes: 4 additions & 0 deletions test/four-xored-types/has-keys-of-B.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

const test: XOR_A_B_C_D = { b: '' } // OK
const testNested: XOR_A_B_C_D_Nested = { b: '' } // OK
6 changes: 6 additions & 0 deletions test/four-xored-types/has-keys-of-C-and-D.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

// @ts-expect-error
const test: XOR_A_B_C_D = { c: '', d: '' }
// @ts-expect-error
const testNested: XOR_A_B_C_D_Nested = { c: '', d: '' }
4 changes: 4 additions & 0 deletions test/four-xored-types/has-keys-of-C.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

const test: XOR_A_B_C_D = { c: '' } // OK
const testNested: XOR_A_B_C_D_Nested = { c: '' } // OK
4 changes: 4 additions & 0 deletions test/four-xored-types/has-keys-of-D.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

const test: XOR_A_B_C_D = { d: '' } // OK
const testNested: XOR_A_B_C_D_Nested = { d: '' } // OK
6 changes: 6 additions & 0 deletions test/four-xored-types/has-no-keys.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { XOR_A_B_C_D, XOR_A_B_C_D_Nested } from './setup'

// @ts-expect-error
const test: XOR_A_B_C_D = {}
// @ts-expect-error
const testNested: XOR_A_B_C_D_Nested = {}
9 changes: 9 additions & 0 deletions test/four-xored-types/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { XOR } from '../../src'

interface A { a: string }
interface B { b: string }
interface C { c: string }
interface D { d: string }

export type XOR_A_B_C_D = XOR<A, B, C, D>
export type XOR_A_B_C_D_Nested = XOR<A, XOR<B, XOR<C,D>>>
4 changes: 0 additions & 4 deletions test/multiple-xored-types/has-keys-of-A-and-C.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions test/multiple-xored-types/has-keys-of-A.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions test/multiple-xored-types/has-keys-of-B.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions test/multiple-xored-types/has-keys-of-C.spec.ts

This file was deleted.

4 changes: 0 additions & 4 deletions test/multiple-xored-types/has-no-keys.spec.ts

This file was deleted.

15 changes: 0 additions & 15 deletions test/multiple-xored-types/setup.ts

This file was deleted.

2 changes: 1 addition & 1 deletion test/shared-and-xored-members/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { XOR } from './../../src'
import type { XOR } from './../../src'

type ForecastAccuracy = XOR<{ '1h': number }, { '3h': number }>

Expand Down
11 changes: 3 additions & 8 deletions test/single-member-objects/setup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { XOR } from './../../src'
import type { XOR } from './../../src'

interface A {
a: string
}

interface B {
b: string
}
interface A { a: string }
interface B { b: string }

export type A_XOR_B = XOR<A, B>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_200 } from './setup';

// @ts-expect-error
const test: XOR_200 = { a: 0, zdt: 0 }
3 changes: 3 additions & 0 deletions test/two-hundred-xored-types/has-keys-of-first.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { XOR_200 } from './setup';

const test: XOR_200 = { a: 0 } // OK
4 changes: 4 additions & 0 deletions test/two-hundred-xored-types/has-keys-of-last-two.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { XOR_200 } from './setup';

// @ts-expect-error
const test: XOR_200 = { zds: 0, zdt: 0 }
3 changes: 3 additions & 0 deletions test/two-hundred-xored-types/has-keys-of-last.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { XOR_200 } from './setup';

const test: XOR_200 = { zdt: 0 } // OK
26 changes: 26 additions & 0 deletions test/two-hundred-xored-types/has-two-hundred-one-params.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { XOR } from '../../src'

// @ts-expect-error
export type XOR_200 = XOR<
{EXTRANEOUS: 0},
{a: 0}, {b: 0}, {c: 0}, {d: 0}, {e: 0}, {f: 0}, {g: 0}, {h: 0}, {i: 0}, {j: 0},
{k: 0}, {l: 0}, {m: 0}, {n: 0}, {o: 0}, {p: 0}, {q: 0}, {r: 0}, {s: 0}, {t: 0},
{aa: 0}, {ab: 0}, {ac: 0}, {ad: 0}, {ae: 0}, {af: 0}, {ag: 0}, {ah: 0}, {ai: 0}, {aj: 0},
{ak: 0}, {al: 0}, {am: 0}, {an: 0}, {ao: 0}, {ap: 0}, {aq: 0}, {ar: 0}, {as: 0}, {at: 0},
{ba: 0}, {bb: 0}, {bc: 0}, {bd: 0}, {be: 0}, {bf: 0}, {bg: 0}, {bh: 0}, {bi: 0}, {bj: 0},
{bk: 0}, {bl: 0}, {bm: 0}, {bn: 0}, {bo: 0}, {bp: 0}, {bq: 0}, {br: 0}, {bs: 0}, {bt: 0},
{ca: 0}, {cb: 0}, {cc: 0}, {cd: 0}, {ce: 0}, {cf: 0}, {cg: 0}, {ch: 0}, {ci: 0}, {cj: 0},
{ck: 0}, {cl: 0}, {cm: 0}, {cn: 0}, {co: 0}, {cp: 0}, {cq: 0}, {cr: 0}, {cs: 0}, {ct: 0},
{da: 0}, {db: 0}, {dc: 0}, {dd: 0}, {de: 0}, {df: 0}, {dg: 0}, {dh: 0}, {di: 0}, {dj: 0},
{dk: 0}, {dl: 0}, {dm: 0}, {dn: 0}, {do: 0}, {dp: 0}, {dq: 0}, {dr: 0}, {ds: 0}, {dt: 0},
{za: 0}, {zb: 0}, {zc: 0}, {zd: 0}, {ze: 0}, {zf: 0}, {zg: 0}, {zh: 0}, {zi: 0}, {zj: 0},
{zk: 0}, {zl: 0}, {zm: 0}, {zn: 0}, {zo: 0}, {zp: 0}, {zq: 0}, {zr: 0}, {zs: 0}, {zt: 0},
{zaa: 0}, {zab: 0}, {zac: 0}, {zad: 0}, {zae: 0}, {zaf: 0}, {zag: 0}, {zah: 0}, {zai: 0}, {zaj: 0},
{zak: 0}, {zal: 0}, {zam: 0}, {zan: 0}, {zao: 0}, {zap: 0}, {zaq: 0}, {zar: 0}, {zas: 0}, {zat: 0},
{zba: 0}, {zbb: 0}, {zbc: 0}, {zbd: 0}, {zbe: 0}, {zbf: 0}, {zbg: 0}, {zbh: 0}, {zbi: 0}, {zbj: 0},
{zbk: 0}, {zbl: 0}, {zbm: 0}, {zbn: 0}, {zbo: 0}, {zbp: 0}, {zbq: 0}, {zbr: 0}, {zbs: 0}, {zbt: 0},
{zca: 0}, {zcb: 0}, {zcc: 0}, {zcd: 0}, {zce: 0}, {zcf: 0}, {zcg: 0}, {zch: 0}, {zci: 0}, {zcj: 0},
{zck: 0}, {zcl: 0}, {zcm: 0}, {zcn: 0}, {zco: 0}, {zcp: 0}, {zcq: 0}, {zcr: 0}, {zcs: 0}, {zct: 0},
{zda: 0}, {zdb: 0}, {zdc: 0}, {zdd: 0}, {zde: 0}, {zdf: 0}, {zdg: 0}, {zdh: 0}, {zdi: 0}, {zdj: 0},
{zdk: 0}, {zdl: 0}, {zdm: 0}, {zdn: 0}, {zdo: 0}, {zdp: 0}, {zdq: 0}, {zdr: 0}, {zds: 0}, {zdt: 0}
>
Loading

0 comments on commit 3d7ffa6

Please sign in to comment.