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.