Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type relationship functions to checker public api #9943

Closed
wants to merge 6 commits into from

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Jul 26, 2016

Fixes #9879

This PR adds the following API surface to the checker:

interface TypeChecker {

    /**
     * Two types are considered identical when
     *  - they are both the `any` type,
     *  - they are the same primitive type,
     *  - they are the same type parameter,
     *  - they are union types with identical sets of constituent types, or
     *  - they are intersection types with identical sets of constituent types, or
     *  - they are object types with identical sets of members.
     *
     * This relationship is bidirectional.
     * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.2) for more information.
     */
    isIdenticalTo(a: Type, b: Type): boolean;
    /**
     * `a` is a ___subtype___ of `b` (and `b` is a ___supertype___ of `a`) if `a` has no excess properties with respect to `b`,
     *  and one of the following is true:
     *  - `a` and `b` are identical types.
     *  - `b` is the `any` type.
     *  - `a` is the `undefined` type.
     *  - `a` is the `null` type and `b` is _not_ the `undefined` type.
     *  - `a` is an enum type and `b` is the primitive type `number`.
     *  - `a` is a string literal type and `b` is the primitive type `string`.
     *  - `a` is a union type and each constituient type of `b` is a subtype of `b`.
     *  - `a` is an intersection type and at least one constituent type of `a` is a subtype of `b`.
     *  - `b` is a union type and `a` is a subtype of at least one constituent type of `b`.
     *  - `b` is an intersection type and `a` is a subtype of each constituent type of `b`.
     *  - `a` is a type parameter and the constraint of `a` is a subtype of `b`.
     *  - `a` has a subset of the structural members of `b`.
     *
     * This relationship is directional.
     * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.3) for more information.
     */
    isSubtypeOf(a: Type, b: Type): boolean;
    /**
     * The assignable relationship differs only from the subtype relationship in that:
     *  - the `any` type is assignable to, but not a subtype of, all types
     *  - the primitive type `number` is assignable to, but not a subtype of, all enum types, and
     *  - an object type without a particular property is assignable to an object type in which that property is optional.
     *
     * This relationship is directional.
     * See [here](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#3.11.4) for more information.
     */
    isAssignableTo(a: Type, b: Type): boolean;
    /**
     * True if `a` is assignable to `b`, or `b` is assignable to `a`. Additionally, all unions with
     * overlapping constituient types are comparable, and unit types in the same domain are comparable.
     * This relationship is bidirectional.
     */
    isComparableTo(a: Type, b: Type): boolean;
    /**
     * Not a formal relationship - returns true if a is an instantiation of the generic type b
     */
    isInstantiationOf(a: GenericType, b: GenericType): boolean;

    /**
     * Returns the declared type of the globally named symbol with meaning SymbolFlags.Type
     *  Returns the unknown type on failure.
     */
    lookupGlobalType(name: string): Type;
    /**
     * Returns the declared type of the globally named symbol with meaning SymbolFlags.Value
     *  Returns the unknown type on failure.
     */
    lookupGlobalValueType(name: string): Type;
    /**
     * Returns the declared type of the named symbol lexically at the position specified with meaning SymbolFlags.Type
     *  Returns the unknown type on failure.
     */
    lookupTypeAt(name: string, position: Node): Type;
    /**
     * Returns the declared type of the named symbol lexically at the position specified with meaning SymbolFlags.Value
     *  Returns the unknown type on failure.
     */
    lookupValueTypeAt(name: string, position: Node): Type;
    /**
     * Returns the type of a symbol
     */
    getTypeOfSymbol(symbol: Symbol): Type;

    getAnyType(): Type;
    getStringType(): Type;
    getNumberType(): Type;
    getBooleanType(): Type;
    getVoidType(): Type;
    getUndefinedType(): Type;
    getNullType(): Type;
    getESSymbolType(): Type;
    getNeverType(): Type;
    getUnknownType(): Type;
    getStringLiteralType(text: string): LiteralType;
    getNumberLiteralType(text: string): LiteralType;
    getFalseType(): Type;
    getTrueType(): Type;
    getNonPrimitveType(): Type;
}

Along with appropriate unit tests.

This differs from the original proposal in that it adds lookupGlobalValueType, lookupValueTypeAt, and getTypeOfSymbol (along with getNumberLiteralType, getFalseType, and getTrueType, which were only enabled after the original issue was opened). Without these, it was difficult (impossible?) to do things like lookup the type of a function foo within a given scope (since this is a type in value space rather than type space) and get the type associated with the symbol returned for a type's property.

@@ -1861,6 +1861,53 @@ namespace ts {
getJsxIntrinsicTagNames(): Symbol[];
isOptionalParameter(node: ParameterDeclaration): boolean;

/**
* Two types are identical iff they are references to the exact same fully qualified, resolved type (usually an alias to ===)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by "usually an alias to ==="?

Copy link
Member Author

@weswigham weswigham Jul 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most often, the identity relation will only be true when a === b. There's only a few other things (identical generic instantiations, I think) not covered by that.

@weswigham
Copy link
Member Author

@ahejlsberg You probably want to weigh in on this.

@weswigham
Copy link
Member Author

weswigham commented Aug 2, 2016

I've added getter methods for numeric literals, true, and false. (Enum members should be lookup-able)

@weswigham weswigham force-pushed the type-relationship-api branch 3 times, most recently from 806ffe1 to acf75c3 Compare August 10, 2016 21:24
@MeirionHughes
Copy link

Could this be rebased and reviewed please?

@Sheyne
Copy link

Sheyne commented Mar 10, 2017

I rebased this branch and all tests pass. You can access my changes at: Sheyne#1.
I didn't really do anything other than resolve a small merge conflict, but I can sign a CLA if need be.

@alexeagle
Copy link
Contributor

+1 @DanielRosenwasser can this one be revived?

@KiaraGrouwstra
Copy link
Contributor

This would be great, would love to see it make it in!

@alexeagle
Copy link
Contributor

ping @DanielRosenwasser this came up again

const symbol = getSymbol(globals, escapeLeadingUnderscores(name), SymbolFlags.Value);
return symbol ? getTypeOfSymbol(symbol) : unknownType;
},
lookupTypeAt: (name, node) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wounder if we should just expose resolveName instead..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing it directly would make public way too many of the internal details, like usage checking, that are built into it. It's very overloaded, and the public API edition of its functionality should be simplified, I think. Looking at this again, I think Type/Value may be better qualifiers on the function names than Type/ValueType, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use resolveName

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. @ahejlsberg can you also take a look please.

@ahejlsberg
Copy link
Member

The thing I find hard to reason about is where to stop. Makes sense to expose the type relationship functions, resolveName (or some simplified version thereof), getTypeOfSymbol, and the built-in type accessor functions. But then what about getUnionType, getIntersectionType, getIndexType, getIndexedAccessType, createTypeReference, getTypeAliasInstantiation, createAnonymousType, createSymbol, etc. Without some of those, the utility of the type relationship functions seems limited as you can only use them if both types you want to compare actually exist in the program. For example, you couldn't compare to string[] because you have no way of manufacturing that type.

Just trying to understand what the guiding principle is here?

@weswigham
Copy link
Member Author

I (way back when this PR was first made) had a separate PR exposing safe APIs to programmatically build types (since the way we build types internally is very lazy and requires an associated node tree, etc) separate from this one. That's the main distinction here - this PR is for comparing and accessing existing types, while I have other work for public APIs for building types. However many things could operate only using existing types (like many lint rules), so the two need not be coupled (this is both easier to expose directly and arguably more immediately useful).

},
getTypeOfSymbol,

getAnyType: () => anyType,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have a new function getBuiltinType instead of all of these..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just getBuiltinTypes() that returns an object literal with all of them, so that it is future-proof.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 6, 2017

We should also add the factories that are needed, e.g. union, intersection, type reference instantiation, anonymous types, createSymbol. createSignature. and have a full proposal for the change.

isSubtypeOf: (a, b) => checkTypeRelatedTo(a, b, subtypeRelation, /*errorNode*/ undefined),
isAssignableTo: (a, b) => checkTypeRelatedTo(a, b, assignableRelation, /*errorNode*/ undefined),
isComparableTo: areTypesComparable,
isInstantiationOf: (a, b) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this one i would say.

},
getTypeOfSymbol,
getUnknownType: () => unknownType,
getStringLiteralType: (text: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider merging these two into one getLiteralType

@kevinbuhmann
Copy link

Any update on this?

@weswigham
Copy link
Member Author

weswigham commented Feb 28, 2018

@kevinphelps The added wish of

We should also add the factories that are needed, e.g. union, intersection, type reference instantiation, anonymous types, createSymbol. createSignature. and have a full proposal for the change.

Is rather complicated, since what we use internally is super easy to misuse, and I, at least, would rather not directly expose those bits as is (esp. given how often we breakingly change them). A comprehensive proposal for a stable public view on type creation/comparison is big. A long time ago I started drafting an API here, but it's hopelessly outmoded now (even moreso than this PR), and I'm not even sure a fluent API is correct. 🙁
It's just a lot of work to put this together in a way we can maintain and other things are higher priority right now.

@typescript-bot
Copy link
Collaborator

Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it.

@SamVerschueren
Copy link

Why didn't we actually go for type relationship APIs in the first place and add the factories later? People (like me) have to use a 3rd party library (like ts-simple-type) which works, but has it's flaws. While the only thing I need (in my use-case) is isTypeAssignableTo so I can leverage the power of TypeScript itself while other libraries have a hard time figuring out assignability between types.

I understand that ideally in the end, everything like the factories is exposed as well. But to be honest, if the type relationship API, which @weswigham did in this PR, was exposed, it would make things much easier for people working with the TypeScript type checker directly.

@DanielRosenwasser
Copy link
Member

If you can tell us a little bit about the use cases, that would be one step in the right direction. We assumed that the relationship APIs alone wouldn't be enough to solve most use cases.

I still am a bit skeptical given that there's other information I've heard people asking for (e.g. tracking the declared type vs. the flow-analyzed type at read positions) which requires more information to be exposed than originally expected.

@SamVerschueren
Copy link

Sure.

So I'm working on tsd which helps in testing type definitions written by hand. @sindresorhus uses it in a ton of his packages already. It's a little like dtslint except that it doesn't use tslint (which is EOL) but leverages the power of the TypeScript compiler.

One thing that I'm now solving is exact type assertions. This means that I want to check that the expected type is exactly the type returned and isn't too wide. For instance, expectType<string | number>(['foo', 'bar'].join(', ')); currently works because string is assignable to string | number. But we want this to fail.

How do I check this? I use the AST to extract the type information of the generic and of the argument. If the argument type (string) is assignable to the expected type (string | number), but the expected type is not assignable to the argument type, the type is too wide.

So I looked into ts-simple-type to test assignability. It works in most use cases, but not for everything. For example, it has a big where it can't detect the assignability between generic types, e.g. it returns true for Observable<string | number> ~ Observable<string>.

I added one line to the type checker which exposed the isAssignableTo method, it then everything works as expected because TypeScript knows all this information and is much better in inferring these things.

So in my use case, only the assignability check would be necessary and would help me an awefull lot. Given the fact that there is a library like ts-simple-type which tries to mimick this, and is used by others, I think it would be very nice if TypeScript exposed this method (and maybe also the others in this PR, but that's not up to me to decide on how useful they are).

@SamVerschueren
Copy link

We assumed that the relationship APIs alone wouldn't be enough to solve most use cases.

I'm not sure what the original use cases are. But I think that for everyone working with the AST, based upon source files, will have all the types available and have enough with the relationships API.

I see them as separates scopes. The scope of this PR is "small" and easy to review. It offers functionality that I can use directly and helps me when working with the AST.

The factory methods on the other hand, as much as I understand them, allow you to create types dynamically and thus not based upon source files, or an already existing AST. This feels to me like a different scope and thus a different PR which can be specced out differently.

This is just my opinion though. You guys have much more knowledge and insights on use cases. I just think that it's a pitty that this PR became stall and people have to use 3rd party packages which try to mimick the TypeScript internals while the core already have all this information.

@Sheyne
Copy link

Sheyne commented Sep 30, 2019

My use case in Sinap (https://github.com/2graphic/sinap-typescript-loader) was to use Typescript as a DSL for writing simple graph interpreters. The UI would allow you to build a graph (as in network, not as in plot) and the valid edges and nodes on the graph depended on the assignability of various types in .TS files in the interpreter.

I've not followed the factories described above, but if they don't allow easily checking assignability from types loaded from source files, then this is another example use case.

@SamVerschueren
Copy link

I also need isIdenticalTo in tsd. Because I really need these private API's, I copied over typescript@3.6.3 entirely to a libraries directory which exposes isIdenticalTo and isAssignableTo (the latter will be available by casting somewhere in the next release probably). It was a decision I didn't want to make, but this was the only way I could leverage the power of the TypeScript APIs and build tsd with all the necessary power.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: Type Relationship API