Proposal: Type Relationship API
weswigham opened this issue ยท 37 comments
They type checker has no public API for checking type relationships or for looking up types in a given scope. This can make writing semantic-analysis informed lint rules very difficult (either because you need to replicate a lot of logic, or because you simply don't have enough information). Since tslint enabled semantic checks we now have a popular consumer who would very much like to be able to perform meaningful semantic comparisons (See this rule for instance).
Proposal
A small new set of public API methods on the type checker:
interface TypeChecker {
isIdenticalTo(a: Type, b: Type): boolean; // Exposes internal identity relationship
isSubtypeOf(a: Type, b: Type): boolean; // Exposes internal structural subtype relationship
isAssignableTo(a: Type, b: Type): boolean; // Exposes internal assignable relationship
isComparableTo(a: Type, b: Type): boolean; // Exposes internal comparable relationship
isInstantiationOf(a: GenericType, b: GenericType): boolean; // While not an internal relationship, it is a straightforward semantic question to ask an answer which requires internal APIs to answer
lookupGlobalType(name: string): Type; // Looks up a global symbol named "name" with meaning SymbolFlags.Type (Identical to getGlobalType but without the expected arity based return value) - returns unknownType on failure
lookupTypeAt(name: string, position: Node): Type // Resolve a symbol lexically at the position specified with meaning SymbolFlags.Type (identical to resolveName, but with less levers) - returns unknownType on failure
getAnyType(): IntrinsicType;
getStringType(): IntrinsicType;
getNumberType(): IntrinsicType;
getBooleanType(): IntrinsicType;
getVoidType(): IntrinsicType;
getUndefinedType(): IntrinsicType;
getNullType(): IntrinsicType;
getESSymbolType(): IntrinsicType;
getNeverType(): IntrinsicType;
getUnknownType(): IntrinsicType;
getStringLiteralType(text: string): StringLiteralType; // When numeric literal types merge, they should have a similar endpoint
}While this isn't a complete set of APIs for manipulating the type checker (I've left off an API to programatically create new types for comparison), it is enough to do meaningful analysis with existing types in the compilation without resorting to brittle (or often wrong) syntax-based solutions.
I was discussing something like this today with @novemberborn in that sometimes we aren't sure why the types are what they are at a particular point, especially when they are contextually inferred.
Would it be possible to also consider potentially offering up additional meta data that would explain the type at a particular node (sort of like good ole SQL "explain plan")?
Would it be possible to also consider potentially offering up additional meta data that would explain the type at a particular node (sort of like good ole SQL "explain plan")?
can you elaborate on the scenario here? there are multiple ways the compiler can find a type. though, all is specified by the spec, the information is not persisted any ways. what we know is a type.
Is there any progress on this? It would be incredibly useful to have. Just an isTypeAssignableTo would be great. Right now the only option is to link to a customly patched version of TypeScript.
As PR #9943 is closed now, is it possible to export only isTypeRelatedTo and *Relation which should cover @raveclassic and @samvv cases, also with identityRelation it will cover:
- tslint rule no-inferrable-type
where only primitive number, string and boolean types are supported
- tslint-consistent-codestyle no-unnecessary-type-annotation (@ajafff)
where
typeToStringis used, however it often gives false negatives, e.g. in cases like{ x: number, y: number }type is not equal to{ y: number, x: number } - fimbullinter/wotan no-useless-assertion and prefer-for-of
- no-inferrable-return-types
also it will cover few closed issues, where authors were waiting for PR #9943 like:
also at least partly it should cover this request: #11728 (comment)
also it will close a few more closed by bot issues.
As another use case, this would enable type checking HTML template systems such as lit-html. For this code:
html`<date-picker .date=${currentDate}></date-picker>`We'd want to assert that typeof currentDate is assignable to ElementTagNameMap['date-picker'].date.
This is applicable to many template systems.
Would love to see some baby steps that could occur in this area. It seems like the tooling that would be enabled by having APIs like this would be tremendous! As @ahejlsberg pointed out, it's hard to know where to stop with these APIs, but I liked @weswigham's suggestion in the original pull request #9943 about separating the "type comparison" apis from the "type building" apis. It seems like the "type building" apis is a big task, but the type comparison apis might be a smaller effort?
Just chatted with @octref about improving the custom element editing experience. I think the need for these APIs is showing up across the editing ecosystem as every template system provider eventually runs into the need to type check templates.
He pointed out that the Vue LSP is working on implementing type checking of templates by doing code generation, running the typescript compiler over the generated code, then mapping diagnostics on that code back to the original source: https://github.com/vuejs/vetur/pull/681/files
Having a type relationship API would make this sort of work simpler, faster, and more robust.
I've just discovered https://www.npmjs.com/package/ts-simple-type by way of https://twitter.com/RuneMehlsen/status/1095401856493330433.
It looks like a reasonable alternative until the official TS api is public.
I was trying to write a lint rule where the following would not be allowed,
declare const x : { foo : string };
//Should have lint error
const y : { foo : string|number } = x;
y.foo = 42;
console.log(x.foo);And had to write a lot of type checking code that's probably super brittle.
I'll give ts-simple-type a whirl
[EDIT]
I just tried ts-simple-type and it... Crashes a lot.
It exceeds the max call stack size regularly and also runs into,
Couldn't find 'typeParameter' for type 'FUNCTION'
When it doesn't crash, it works great, though.
@AnyhowStep if you have reasonably-sized repros please file issues on ts-simple-type for any crashes you find
I'm wondering what the status is on this proposal. Something like isAssignableTo(a: Type, b: Type): boolean; would really help me out on a project I'm working on. I'm using ts-simple-type, but it fails to calculate assignability for RxJS Observables. Created an issuer in the repo runem/ts-simple-type#54.
It's exposed in internal type checker API now: https://github.com/microsoft/TypeScript/pull/33263/files#diff-c3ed224e4daa84352f7f1abcd23e8ccaR525-R527
It's not publicly documented, is it?
Lol, nope. It's not public at all.
I need a crying emoji
We need this. Please add this.
I would also like add a +1 on this feature! Our use case was a lint rule which checks the compatibility of return types, and this feature would also allow other powerful semantic linter rules.
May I ask if you have any concerns in a design perspective preventing this feature to move forward? I've also noticed the internal API before I found this thread, but I was slightly afraid that this internal API could get discontinued at one point.
If you do in the long-run want to expose these kind of type checking features, we might feel a bit more safer to use the internal ones for now.
I thought I wanted:
isComparableTo(a, b) && isComparableTo(b, a)But I want something that returns true when testing:
{ a: string }
{ a?: string }So, what do I want?
@SlurpTheo do you want to know whether the former is assignable to the latter? i.e. that this type checks
declare let former: { a: string };
declare let latter: { a?: string };
latter = former;Well, given two ts.Type values I want to ask the ts.TypeChecker something
Right, but before asking which method to call, you first gotta figure out what operation you want it to perform. i.e. when it should return true and when it should return false
Goal: Are these the same shape (neglecting partiality)?
This feels dirtier every second I think about it -- it seems I'm asking for:
latter = former
former = latterI don't think you want that, because that doesn't type check: https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEEQBd4DMD2MC2IYC54BveKQgZ2RgEsA7Ac3gF8BuAKFElgSVWmWR5CJKAH4KVOo1Zs2-QTHgBedFlwx2mHHmWIoAvOyA
If that is what you want, then isAssignableTo might be the method you need.
This can make writing semantic-analysis informed lint rules very difficult
Such rules would be useful, but a bit limited given that ESlint works on a per-file basis. I don't think the full type information would be available - just what can be learnt from parsing that single file.
@robatwilliams it's entirely possible to "work around" the single file limitation and provide type-aware lint rules within ESLint.
It would be great to expose these โ most are already implemented and used internally. Specifically, I'd like lookupGlobalType("Array")
My use-case that needs these features but not type construction APIs is about traversing types to use Typescript as a kind of interface definition language.
I need to accomplish the following tasks:
- Break a discriminated union type down into its member parts.
- Find the discriminant property of the union type.
- Traverse all properties of a plain object type or interface type, looking for instances of a few known special relation types:
RelationPointer<Table>RelationPointer<Table>[]UUIDof<Table>UUIDof<Table>[]
- Traverse the return types of methods of interface types, including traversing promise return values.
- Detect and understand primitive types like
string,numberetc. - Detect and understand "type brands" of primitives like
string & { __meta?: SomeMetadataType }
To accomplish these tasks, I would like to use the following APIs proposed in this discussion:
- All APIs for fetching primitive types.
lookupGlobalType('Array')- needed to understand if a type is an array type.isArrayTypeis even better, but not part of the original proposal.
lookupGlobalType('Promise')- needed to traverse promises.getAwaitedTypeis even better, but not part of the original proposal.
isAssignableTo- needed to understand branded primitive types.isIdenticalTo- needed to understand non-branded primitive types.
I am happy to start a new PR (I have a branch here: https://github.com/microsoft/TypeScript/compare/main...justjake:jake--9879-type-relation-api?expand=1
Or, I can pick up the languished branch #9943
But first, can we conclude that this idea has merit, and move from "Suggestion" to "Backlog"?
Right now I believe I can work around the lack of some of these APIs by doing something like this:
// landmarks.ts
export type GlobalArray<T> = Array<T>
export type GlobalPromise<T> = Promise<T>
export type GlobalString = string
export type GlobalNumber = number// analyze.ts
const program = ts.createProgram(['landmarks.ts', /* ... */], config)
interface LandmarkQuery {
filePath: string
exportName: string
}
interface LandmarkResult extends LandmarkQuery {
sourceFile: ts.SourceFile
symbol: ts.Symbol
type: ts.Type
}
export function getProgramLandmarkTypes<
T extends Record<string, LandmarkQuery>
>(program: ts.Program, landmarks: T): { [K in keyof T]: LandmarkResult } {
const typeChecker = program.getTypeChecker()
const result: { [K in keyof T]: LandmarkResult } = {} as any
for (const [landmarkName, { filePath, exportName }] of Object.entries(
landmarks
)) {
const sourceFile = program.getSourceFile(rootPath(filePath))
if (!sourceFile) {
throw new Error(`Cannot find source file ${filePath}`)
}
const fileSymbol = typeChecker.getSymbolAtLocation(sourceFile)
if (!fileSymbol) {
throw new Error(
`Could not find type symbol for schema file ${Files.schemas}`
)
}
const symbol = fileSymbol.exports?.get(exportName as ts.__String)
if (!symbol) {
throw new Error(
`Could not find export symbol ${exportName} in ${filePath}`
)
}
const type = getTypeOfSymbolOrThrow(typeChecker, symbol, "declared")
result[landmarkName as keyof T] = {
filePath,
exportName,
sourceFile,
symbol,
type,
}
}
return result
}
const landmarks = getProgramLandmarkTypes(program, { string: ..., number: ..., })But, computing assignability is a little beyond my means at the moment.
Just trying to keep this alive ๐
It would be immensely useful to have at least the isTypeAssignableTo method available to be used from typescript-eslint. My use case is a rule that prevents dangerous type assertions (i.e., that allows to do 'a' as string | number but not string as 'a'. See typescript-eslint/typescript-eslint#7173
Fun fact: https://github.com/JoshuaKGoldberg/TypeStat has been using TypeScript's internal isTypeAssignableTo for over 4 years now (JoshuaKGoldberg/TypeStat@0129b6d). It does so by rewriting typescript.js on disk. Over those >4 years, TypeStat has never had any issue with the API changing or having bugs - stateful or otherwise.
Here's a rough list of the typescript-eslint / area issues & PRs that are blocked or restricted on a TypeScript type assignability API:
- eslint-community/eslint-plugin-eslint-plugin#340
- JoshuaKGoldberg/eslint-plugin-expect-type#18
- #54148 (comment)
- typescript-eslint/typescript-eslint#277
- typescript-eslint/typescript-eslint#709
- typescript-eslint/typescript-eslint#1056
- typescript-eslint/typescript-eslint#3440
- typescript-eslint/typescript-eslint#4978
- typescript-eslint/typescript-eslint#5647
- typescript-eslint/typescript-eslint#6374 (in that it presents a known edge case bug, see PR description)
- typescript-eslint/typescript-eslint#6442
- typescript-eslint/typescript-eslint#7173
- typescript-eslint/typescript-eslint#7236
- typescript-eslint/typescript-eslint#7652
- typescript-eslint/typescript-eslint#7742
- (edit 10/19) typescript-eslint/typescript-eslint#7757
Note that this is definitely incomplete. For many of the use cases we could have filed issues for, people didn't because they knew it was blocked on #9879. typescript-eslint/typescript-eslint#7742 is an example.
To my knowledge, we've almost never (perhaps truly never) had a need for a relationship API other than assignability. We're trying to be good citizens and not taking the TypeStat strategy of cracking open TypeScript's files for the isTypeAssignableTo API. But this is really painful.
Edit: @jakebailey and I just noticed that checker.isTypeAssignableTo does exist now - but it's marked as internal. So although libraries like TypeStat no longer need to manually rewrite files to get to isTypeAssignableTo, it's still not "public".
Just for visibility, I'm planning on having typescript-eslint take a dependency on the private checker.isTypeAssignableTo API in its next major version (v7): typescript-eslint/typescript-eslint#7936. Hopefully that will give helpful feedback on where to take a future public API later on. Someone should go and yell at us over there if that's a bad idea. ๐
I just merged #56448 which makes isTypeAssignableTo public on the checker for TS 5.4. Check out the PR description and associated links for the justification; the design meeting feedback was all positive. Hard not to be when the external usage has made it load-bearing!
This obviously isn't the entire proposal in the thread (which contains many more relationships and getters), but it is likely the most valuable function given all of the feedback above. It also has the benefit of having been around for a very long time with the same name and behavior, so is a good option for those looking to just "start using it now".
This issue is nowhere near closed, of course. Please do provide feedback for the rest!
Is there any way to use isTypeAssignableTo inside a ts file?
I'm looking for a way to check that a type I'm maintaining is assignable to the type of a library (For a unified data model that is extensible)
I simply want typescript to error if my provided type is not assignable to the library type
It seems right now I can do this
const product = null as unknown as ProductCatalog as SfProductCatalogItem; // Error if ProductCatalog and SfProductCatalogItem are not compatible
But it's not typescript only and will polute the output
You're effectively asking for #23689/#40468, but you can check this without any runtime emit using extends and a type which only accepts true: Playground Link

