Skip to content

A fork of the Typescript compiler, adding support for an infer-by-usage type Q

License

Notifications You must be signed in to change notification settings

WhiteAbeLincoln/TypeScript

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fork Details

This is a fork of the TypeScript compiler adding support for an inferred variable type Q, used for automatically determining the shape for a database query.

Behavior of Q

It is intended that Q be introduced as the return type of a query function, i.e. const res: Promise<Q> = query('https://example.com/json').
We determine the proper shape of Q at runtime through type inference:

  1. If we assign Q to a variable of type T, then we can infer/constrain that Q must be assignable to T
    const res: string = await query('...') implies that the query will return a string
  2. If we use dot notation to access a property p on Q, then we infer that a property p exists on Q
    If we have a q : Q, then const r = q.p implies r : Q and q : Q & { p: Q }
    Combining this with property 1, const r: string = q.p implies q : Q & { p: string }
  3. Q must only be a JSON compatible type, i.e. classes, functions, and other stateful objects are not supported
    Because of this, if we call a method such as map on a variable a : Q, then we can infer that Q must be an array, since the only JSON type with a map method is an array

Q can be thought as an opt-in top type to enable Hindley-Damas-Milner type inference?

Examples

We use the symbols <: and :> to denote upper and lower type bounds respectively.

const res = await query('...')

res has some unknown type α <: Q

const blah = res.bar

now we know that α :> { bar: β }, where β <: Q

declare const add(x: number, y: number): number
add(blah, 5)

Because blah: β is used in a position requiring a number, we know β :> number, and consequently α :> { bar: number }

res.foo.map(x => add(x, 1))

This is a more complicated example: First we see that the foo property is accessed, so we know γ <: Q, α :> { bar: number, foo: γ }
Next the map method is called so we know δ <: Q, γ :> Array<δ>, and x: δ in the lambda function. An array is the only option because we know that Q must be a valid JSON type and so must not contain arbitrary functions 1.

Finally, δ is constrained by add, so we know δ :> number, γ :> Array<number> and consequently α :> { bar: number, foo: Array<number> }

declare const fn1(x: number)
declare const fn2(x: string)

fn1(res.baz)
fn2(res.baz)

Here we pass the same type into two functions taking different types.
We know at first that ε <: Q, α :> { bar: number, foo: Array<number>, baz: ε }
By passing ε into a function taking type number, we restrict ε to at least number, or ε :> number.
However, on the next line we pass ε into a function taking a string.
This means that ε must be restricted to some more general type which is the supertype of both string and number, so ε :> string & number. Since string and number are different domains, their intersection is empty, so the given type of ε is never, the bottom type in TypeScript. Therefore, α :> { bar: number, foo: Array<number>, baz: never }.

if (typeof res.qux === 'string') {
    // do something with res.qux
} else {
    add(res.qux, 5)
}

Here we have an example of a subtype which can be a union.
We introduce the type variable ζ <: Q, and restrict alpha α :> { bar: number, foo: Array<number>, baz: string & number, qux: ζ }.
We have an if statement which splits the minimum type of ζ into two possibilities.
In the true branch, we know that ζ :> string, since the typeof check restricts to strings.
In the false branch, we don't know any information initially, but the call to add restricts ζ :> number.
Finally, at the end of the if statement we can union the two possibilities to get the full minimum type: ζ :> number | string, so α :> { bar: number, foo: Array<number>, baz: string & number, qux: string | number }.

Type assertions should also introduce minimum bounds.

const x = (res as { quux: string }).quux

This restricts res to have a minimum of { quux: string }, which combines with the other restrictions on res to make α :> { bar: number, foo: Array<number>, baz: string & number, qux: string | number } & { quux: string }

At the end of typechecking, we should have a fully constructed minimum bound type for α, which was introduced by our query call. We write that type to a file as part of compiling, and send it along with the query at runtime, so that the database knows what the minimum type bound is and can send back data fitting the proper shape. Which variable to emit and unique names would be determined by functions that return a pure Q type, variables that are declared with a Q type, etc. Since every usage of Q must begin somewhere, and any usage of Q will start restricting the type, an unused Q introduced by a keyword can be considered the beginning.

This does break one of TypeScript's core rules, which is no type-directed emit, so there is no chance that this gets merged into master. However, it is conceivable that a compiler plugin or external tool can read the final type of the query variable and emit that type to a file.

Edge Cases

In some cases it is hard to determine whether a property access is an array or just an object with a numeric property:

declare const foo: Q
const x = foo[0] // is foo an Array<Q>, or is it an object { 0: Q }

There are two options here: we could use the strict interpretation and say that β <: Q, α <: Q, β :> { 0: α }, foo: β, or we could take the interpretation that would be more useful for the imperative programmer, and say β <: Q, α <: Q, β :> Array<α>, foo: β. Ideally this would be a compiler flag.
Alternatively the user could be required to use a type assertion: const x = (foo as Array<Q>)[0] or const x = (foo as { 0: Q })[0], or const x = (foo as [Q])[0].
There are similar cases in regular TypeScript for tuples vs arrays, so we should endeavor to match that behavior.

Implimentation Ideas

  1. Add a new top type Q (the actual keyword may differ), which behaves similar to any
  2. The typescript compiler should produce a program graph with types as part of the compilation/typechecking process (not sure if explicit), so we can rely on using that.
  3. we can use some internal methods (such as symbolWalker and visitType) to traverse and extract the section of the program that deals with symbols/expressions/statements of type Q
  4. After we have our own subgraph of just the relevant program, we apply some algorithm to determine what each instance of Q should be, and build up the final types.

Or we can go out of band using a program like ANTLR or Babel to statically analyze typescript source code. The problem with doing this is that we would have to analyze the entire program and construct our own program graph. This would mean reimplementing a lot of typescript's type system.

The problem with modifying TypeScript's compiler is that it is absolutely huge, and there is no public documentation on how to modify or add to it.

References

Notes on adding new types

Glossary

Substitution Type

Substitution types are created for type parameters or indexed access types that occur in the true branch of a conditional type. For example, in T extends string ? Foo<T> : Bar<T>, the reference to T in Foo<T> is resolved as a substitution type that substitutes string & T for T. Thus, if Foo has a string constraint on its type parameter, T will satisfy it. Substitution types disappear upon instantiation (just like type parameters).

Conditional Type

C extends E ? T : F
Immediately resolve if both check (C) and extend (E) are non-generic

  • if E is unknown, any (or inferred), return true branch (T), since all things extend any and inferred
  • if C is any (or inferred), return union of T and F, since it matches anything

Important functions in checker.ts

  • getUnionType :: gets the type of a union from flags and type array
  • addTypeToUnion :: adds a type to a union
  • addTypeToIntersection :: adds a type to an intersection
  • getIntersectionType :: gets the type of an intersection from type array
  • isSimpleTypeRelatedTo, isTypeRelatedTo :: allows checking if two types are related using some relation (such as identity - they are equal or equivalent, subtype, strict subtype, assignable, comparable)
  • getReturnTypeFromBody :: infers a return type from a function body
  • checkAndAggregateReturnExpressionTypes :: helper fn that actually infers the return types from return statements in function body
  • getSubstitutionType :: gets the substitution type for a conditionditional type
  • getConditionalType :: gets the conditional type
  • getTypeOfSymbol :: gets the type of a symbol
  • checkSourceElement :: checks each line of a source file, good for seeing all of the checker functions
  • checkSourceFile :: typechecks a source file

Things to look for in checker.ts

  • TypeFlags.Any
  • anyType
  • TypeFlags.AnyOrUnknown :: common behavior between any and unknown types, which our inferred type will share
  • isTypeAny :: predicate to check if a type is any

TypeScript

Build Status VSTS Build Status npm version Downloads

TypeScript is a language for application-scale JavaScript. TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications for any browser, for any host, on any OS. TypeScript compiles to readable, standards-based JavaScript. Try it out at the playground, and stay up to date via our blog and Twitter account.

Find others who are using TypeScript at our community page.

Installing

For the latest stable version:

npm install -g typescript

For our nightly builds:

npm install -g typescript@next

Contribute

There are many ways to contribute to TypeScript.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact [email protected] with any additional questions or comments.

Documentation

Building

In order to build the TypeScript compiler, ensure that you have Git and Node.js installed.

Clone a copy of the repo:

git clone https://github.com/microsoft/TypeScript.git

Change to the TypeScript directory:

cd TypeScript

Install Gulp tools and dev dependencies:

npm install -g gulp
npm ci

Use one of the following to build and test:

gulp local             # Build the compiler into built/local.
gulp clean             # Delete the built compiler.
gulp LKG               # Replace the last known good with the built one.
                       # Bootstrapping step to be executed when the built compiler reaches a stable state.
gulp tests             # Build the test infrastructure using the built compiler.
gulp runtests          # Run tests using the built compiler and test infrastructure.
                       # You can override the specific suite runner used or specify a test for this command.
                       # Use --tests=<testPath> for a specific test and/or --runner=<runnerName> for a specific suite.
                       # Valid runners include conformance, compiler, fourslash, project, user, and docker
                       # The user and docker runners are extended test suite runners - the user runner
                       # works on disk in the tests/cases/user directory, while the docker runner works in containers.
                       # You'll need to have the docker executable in your system path for the docker runner to work.
gulp runtests-parallel # Like runtests, but split across multiple threads. Uses a number of threads equal to the system
                       # core count by default. Use --workers=<number> to adjust this.
gulp baseline-accept   # This replaces the baseline test results with the results obtained from gulp runtests.
gulp lint              # Runs eslint on the TypeScript source.
gulp help              # List the above commands.

Usage

node built/local/tsc.js hello.ts

Roadmap

For details on our planned features and future direction please refer to our roadmap.

Footnotes

  1. TODO: Currently we require an assertion such as (res.foo as inferred[]).map(...) for the map call to be valid. We could have a lookup for the set of properties on String, Number, Boolean, Array, Object, and check which property matches, but that only works if we know that the property we are using is not an object with arbitrary properties. How could we tell the difference between (any[]).map.bind and (object).map.bind?

About

A fork of the Typescript compiler, adding support for an infer-by-usage type Q

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 100.0%