export type Diff<T, U> = T extends U ? never : T
-
-
Save sliminality/3ee6b87255b85c996b2f162fba10e768 to your computer and use it in GitHub Desktop.
/** | |
* Script to extract dependency graph from a codebase with an entry point. | |
*/ | |
import { | |
createCLICommand, | |
startIfMain, | |
runCommand, | |
createFlagMap, | |
t, | |
} from "../cli/framework" | |
import * as fs from "fs-extra" | |
import * as logger from "../shared/logger" | |
import * as pathLib from "path" | |
// import { promptToConfirm } from "../cli/utils" | |
import { rootPath } from "../cli/utils" | |
import * as ts from "typescript" | |
const flags = createFlagMap({ | |
entry: { | |
description: "Entry point for dependency analysis", | |
schema: t.string(), | |
default: () => rootPath("src/client/main.ts"), | |
}, | |
}) | |
const command = createCLICommand({ | |
description: "Analyze dependencies in app", | |
flags, | |
async run({ entry }) { | |
// Build a program. | |
const options = { | |
allowJs: true, | |
strictNullChecks: true, | |
noImplicitThis: true, | |
allowSyntheticDefaultImports: false, | |
resolveJsonModule: true, | |
removeComments: true, | |
downlevelIteration: true, | |
sourceMap: true, | |
jsx: ts.JsxEmit.React, | |
target: ts.ScriptTarget.ES5, | |
lib: ["es6", "es2017", "dom", "scripthost", "esnext.asynciterable"], | |
incremental: true, | |
outDir: "./build/", | |
} | |
const program = ts.createProgram([entry], options) | |
const checker = program.getTypeChecker() | |
// Get program source files without node_modules. | |
const sourceFiles = program | |
.getSourceFiles() | |
.filter(file => file.fileName.startsWith(rootPath("src"))) | |
// Map from file to its imports. | |
const imports = new Map<ts.SourceFile, Set<ts.SourceFile>>() | |
// Map from absolute file paths to SourceFile objects. | |
const sourceFilesMap = new Map<string, ts.SourceFile>() | |
for (const file of sourceFiles) { | |
if (!file.isDeclarationFile) { | |
sourceFilesMap.set(relativePath(file.fileName), file) | |
imports.set(file, getImportsForFile({ file, checker })) | |
} | |
} | |
printDependencies(imports) | |
}, | |
}) | |
function printDependencies(imports: Map<ts.SourceFile, Set<ts.SourceFile>>) { | |
for (const [file, dependencies] of imports) { | |
const name = relativePath(file.fileName) | |
console.group(name) | |
for (const dependency of dependencies) { | |
if (!dependency.fileName) { | |
throw new Error("no dependency filename") | |
} | |
logger.log(relativePath(dependency.fileName)) | |
} | |
console.groupEnd() | |
} | |
} | |
function getImportsForFile(args: { | |
file: ts.SourceFile | |
checker: ts.TypeChecker | |
}): Set<ts.SourceFile> { | |
const { file, checker } = args | |
const imports = new Set<ts.SourceFile>() | |
for (const importSymbol of getImports({ file, checker })) { | |
// Get the declaration to find out which source file it is coming from. | |
const declarations = importSymbol.getDeclarations() | |
if (!declarations) { | |
throw new Error(`No declarations for symbol ${importSymbol.name}`) | |
} | |
// If there are multiple declarations (.d.ts files), use the first one. | |
const [declaration] = declarations | |
const importSource = declaration.getSourceFile() | |
imports.add(importSource) | |
} | |
return imports | |
} | |
function getImports(args: { | |
file: ts.SourceFile | |
checker: ts.TypeChecker | |
}): Array<ts.Symbol> { | |
const { file, checker } = args | |
const results: Array<ts.Symbol> = [] | |
const visitNode = (node: ts.Node) => { | |
if ( | |
ts.isImportDeclaration(node) || | |
ts.isImportEqualsDeclaration(node) || | |
ts.isExportDeclaration(node) | |
) { | |
const moduleName = getExternalModuleName(node) | |
if (!moduleName) { | |
return | |
} | |
// Check that name is not an alias definition, e.g. `import x = y` | |
if (!ts.isStringLiteral(moduleName)) { | |
return | |
} | |
// Verify the module name can be resolved. | |
const moduleSymbol = checker.getSymbolAtLocation(moduleName) | |
if (moduleSymbol) { | |
results.push(moduleSymbol) | |
} | |
} | |
// Recur with child. | |
ts.forEachChild(node, visitNode) | |
} | |
ts.forEachChild(file, visitNode) | |
return results | |
} | |
function getExternalModuleName(node: ts.Node): ts.Expression | undefined { | |
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { | |
return node.moduleSpecifier | |
} | |
if (ts.isImportEqualsDeclaration(node)) { | |
const { moduleReference } = node | |
if (ts.isExternalModuleReference(moduleReference)) { | |
return moduleReference.expression | |
} | |
} | |
} | |
/** | |
* Return a path relative to the project root. | |
*/ | |
function relativePath(absolute: string): string { | |
return pathLib.relative(rootPath(""), absolute) | |
} | |
startIfMain(module, (name, argv) => runCommand(name, command, argv)) | |
// node.body.statements[26].declarationList.declarations[0].initializer.expression.expression.expression.expression |
Creating them is annoying. Can't figure out how to preserve key names.
// =============================================================================
// discriminate.
// =============================================================================
/**
* Turns an object with two optional keys into a discriminated union.
*
* @example
* declare var localResults: SearchResults | undefined
* declare var serverResults: SearchResults | undefined
* const { a: local, b: server } = utils.discriminate({ a: localResults, b: serverResults })
*/
export function discriminate<A, B, KeyA, KeyB>(
obj: {
a?: A
b?: B
},
key1: KeyA,
key2: KeyB
):
| { a: A; b: B }
| { a: A; b: undefined }
| { a: undefined; b: B }
| { a: undefined; b: undefined } {
const { a, b } = obj
// Both keys are defined.
if (a && b) {
return { a, b }
}
// Only one key is defined.
if (a) {
return { a, b: undefined }
}
if (b) {
return { a: undefined, b }
}
// Neither key is defined.
return { a: undefined, b: undefined }
}
Filters an object to the keys whose values extend the given type.
type Obj = {
a: string,
b: boolean,
c: number,
d: string,
}
type OnlyStringKeys = ObjectFilter<Obj, string>
const x: OnlyStringKeys = {
a: "one string",
d: "another string",
}
export type ObjectFilter<O extends object | undefined, T> = Pick<
O,
{
[K in keyof O]: O[K] extends T ? K : never
}[keyof O]
>
TypeScript's library definition of Array.prototype.filter
does not propagate the result of logical operations (like !
and ||
) on type predicates (like token is MentionToken
).
That is, passing isMentionToken
directly to filter
works as expected:
declare var text: Array<TextToken>
const onlyMentions = text.filter(isMentionToken)
// refined to Array<MentionToken>
...but negating isMentionToken
in a lambda does not flow the refinement through:
declare var text: Array<TextToken>
const withoutMentions = text.filter(token => !isMentionToken(token))
// still Array<TextToken>
In this case, we could do something like
function convertTextValue(text: TextValue) {
const withoutMentions = text.filter(
(token): token is Diff<TextToken, MentionToken> => !isMentionToken(token)
)
but this is brittle because user-defined type guards are unchecked: if we subsequently modify the body of the predicate to add other checks, but forget to update the assertion, TypeScript won't catch it.
A similar problem arises when filtering out code annotations and mention annotations. In that case, writing
declare var annotations: Array<TextAnnotation>
const filtered = annotations.filter(
annotation => !isCodeAnnotation(annotation) && !isMentionAnnotation(annotation)
)
// filtered is still Array<TextAnnotation>
is harder to read, and doesn't capture the refinement.
Instead, utils.filterOut
allows application code to use existing predicates to subtract types from a union. Likewise, utils.isAnyOf
combines predicates, propagating refinements to filter
.
export function filterOut<T, Removed extends T>(
array: Array<T>,
isRemoved: (item: T) => item is Removed
): Array<Diff<T, Removed>> {
return array.filter((item): item is Diff<T, Removed> => !isRemoved(item))
}
export function isAnyOf<A, B, C, D>(
pred1: (item: unknown) => item is A,
pred2: (item: unknown) => item is B,
pred3?: (item: unknown) => item is C,
pred4?: (item: unknown) => item is D
) {
return (item): item is A | B | C | D => {
if (pred1(item) || pred2(item)) {
return true
}
if (pred3 && pred3(item)) {
return true
}
if (pred4 && pred4(item)) {
return true
}
// ...add more cases as needed here.
return false
}
}
/** | |
* Towards a reasonable TypeScript Result<T, E> ADT. | |
*/ | |
/** | |
* Towards a reasonable TypeScript Result<T, E> ADT. | |
*/ | |
import { Assert, SuccessOrFail } from "../../shared/typeUtils" | |
export type Result<T, E> = (Success<T> | Fail<E>) & ResultBase<T, E> | |
interface ResultBase<T, E> { | |
error?: NonNullable<E> | |
map<U>(f: (value: T) => U): Result<U, E> | |
flatMap<U>(f: (value: T) => Result<U, E>): Result<U, E> | |
} | |
class Success<T> implements ResultBase<T, never> { | |
public error: undefined | |
constructor(public value: T) {} | |
public map<U>(f: (value: T) => U): Result<U, never> { | |
return new Success(f(this.value)) | |
} | |
public flatMap<U>(f: (value: T) => Result<U, never>) { | |
return f(this.value) | |
} | |
public isOk(): this is Success<T> { | |
return true | |
} | |
} | |
class Fail<E> implements ResultBase<never, E> { | |
constructor(public error: NonNullable<E>) {} | |
public map<U>(f: (value: never) => U): Result<never, E> { | |
return this | |
} | |
public flatMap<U>(f: (value: never) => Result<U, E>) { | |
return this | |
} | |
public isOk(): this is Success<never> { | |
return false | |
} | |
} | |
export const Result = { Success, Fail } | |
/** | |
* Examples | |
*/ | |
type Employee = { name: string; dog: string | undefined } | |
type EmployeeError = "Not Ivan" | "Does not own dog" | "Dog transfer failed" | |
declare var result: Result<Employee, EmployeeError> | |
export type Assert<T, V extends T> = V | |
if (result.error) { | |
type Assert1 = Assert<EmployeeError, typeof result.error> | |
result.value // ExpectError | |
} else { | |
type Assert1 = Assert<undefined, typeof result.error> | |
type Assert2 = Assert<Employee, typeof result.value> | |
} | |
// Can also be refined with `isOk` | |
if (result.isOk()) { | |
type Assert1 = Assert<undefined, typeof result.error> | |
type Assert2 = Assert<Employee, typeof result.value> | |
} else { | |
type Assert1 = Assert<EmployeeError, typeof result.error> | |
result.value // ExpectError | |
} | |
// map | |
const transferSimba = (e: Employee) => { | |
return { name: e.name, hasSimba: true } as const | |
} | |
const nextResult = result.map(transferSimba) | |
if (nextResult.isOk()) { | |
type Assert1 = Assert<undefined, typeof nextResult.error> | |
type Assert2 = Assert< | |
{ name: string; hasSimba: true }, | |
typeof nextResult.value | |
> | |
} else { | |
type Assert1 = Assert<EmployeeError, typeof nextResult.error> | |
nextResult.value // ExpectError | |
} | |
// map | |
const transferSherlock = (e: Employee) => { | |
return Math.random() > 0.5 | |
? new Result.Success({ name: e.name, hasSherlock: true }) | |
: new Result.Fail("Dog transfer failed" as const) | |
} | |
const withSherlockMaybe = result.flatMap(transferSherlock) | |
if (withSherlockMaybe.isOk()) { | |
type Assert1 = Assert<undefined, typeof withSherlockMaybe.error> | |
type Assert2 = Assert< | |
{ name: string; hasSherlock: boolean }, | |
typeof withSherlockMaybe.value | |
> | |
} else { | |
type Assert1 = Assert<EmployeeError, typeof withSherlockMaybe.error> | |
withSherlockMaybe.value // ExpectError | |
} |
Given some type K <: string
, construct a record containing exactly one1 field of key K
and value V
.
type SingletonRecord<Key extends string, V> = {[_ in Key]: V}
const x: SingletonRecord<"a", {test: number}> = {a: {test: 1}}
const y: {a: {test: number}} = x
const shouldFail: SingletonRecord<"a", {test: number}> = {
a: {test: 1},
// Error: Object literal may only specify known properties,
// and 'b' does not exist in type 'SingletonRecord<"a", { test: number; }>'
b: true,
}
[1]: "Exactly one" is, of course, something of a lie because TypeScript does not have exact object types. The idea here is to avoid ending up with an indexed type with no constraints on the keys.