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

Suggestion: Type-check statement to verify type assignability #30809

Open
5 tasks done
jet2jet opened this issue Apr 8, 2019 · 7 comments
Open
5 tasks done

Suggestion: Type-check statement to verify type assignability #30809

jet2jet opened this issue Apr 8, 2019 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jet2jet
Copy link

jet2jet commented Apr 8, 2019

Search Terms

type check statement compile (compilation) time

Suggestion

I wanted to check compatibilities for types from different (especially external) modules in compilation time.

import { SomeType } from 'some-external';
// ... something statement here to check if SomeType is not changed
// by update for 'some-external' or etc.

To solve it, I suggest to add type-check statement like following:

// Compile error if 'SomeType' does not satisfy the constraint 'WantedType'
//   (similar to error TS2344)
type assert SomeType extends WantedType;
// The message "Unexpected 'SomeType'" will be output if the error occurs
// - The trailing expression (message clause) must be a valid string literal
// (the message clause is not necessary for this suggestion
//  but would make easier to solve the error...)
type assert SomeType extends WantedType, "Unexpected 'SomeType'";
  • The type-check process should be same as the process for type constraints in type parameters.
  • There is already type statement, but the above statements are not conflicted because type statements requires = after BindingIdentifier (or TypeParameters).
  • Similar to [Proposal] Type assertion statement (type cast) at block-scope level #10421, but this type assert only check types; it does not cast variables.
  • extends keyword may confuse to one used in conditional type. (not ambiguous?)
    • For type assert A extends B, both A and B should accept conditional type.
  • (type assert is somewhat inspired by other languages' assert statements (especially static_assert in C/C++), but I don't know whether the words type assert are the best...)

Use Cases

  • Check compatibilities for types simply and easily
    • Useful to avoid using incompatible package versions with another packages/modules.
  • Assert types explicitly with more human-readable
    • This would not be useful for compilers/implementers, but would be useful for developers to read codes.
  • Test type definition (e.g. for unit test)

Currently we can check types statically as following:

import { SomeType } from 'some-external';
// Helper definition to check type
type TypeCheckerOfWantedType<T extends WantedType> = T;
// Error if 'SomeType' does not satisfy 'WantedType'
type CheckResultForSomeType = TypeCheckerOfWantedType<SomeType>;
// this is necessary to avoid "'CheckResultForSomeType' is declared but never used"
declare global { var _dummyVariableForCheckSomeType: CheckResultForSomeType; }

While no JavaScript codes are generated from this code, this is more complex to check.

Examples

// Must not be an error
type assert Element extends Node;
// Error in 'es5', pass in 'es2015'
type assert ObjectConstructor extends { assign: (...args: any[]) => any; },
  'Object.assign() is not available';

import { User } from 'db-library';
// Error if User does not have 'id' member with type assignable to 'number'
type assert User extends { id: number; }, 'Unexpected User type';

const a = someFunction();
if (typeof a === 'string') {
    // treat 'a' as 'string'
} else {
    // Error if narrowed type of 'a' does not have '{ data: string }'
    type assert typeof a extends { data: string; };
    // (this assert does not mean that type of 'a' is treated as '{ data: string }' here)
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jack-williams
Copy link
Collaborator

This should be covered by #23689

@dsherret
Copy link
Contributor

dsherret commented Apr 8, 2019

This is already possible to do with conditional types. See here: https://stackoverflow.com/a/55046618/188246 (Rolled up in my poorly named library here).

// or use AssertTrue type alias
import { Has, assert } from "conditional-type-checks";
import { SomeType } from "some-external";

assert<Has<SomeType, WantedType>>(true);

@jet2jet
Copy link
Author

jet2jet commented Apr 8, 2019

Thanks for comments.
I think #23689 and assert function from conditional-type-checks are good approach, but they seem that the runtime code (calling function; even if stripped by optimization) and/or unused type definition (e.g. type XXX = typeof assert<...>) are required to check types.

In my suggestion, both calling function and unused type definition are not necessary.

@dsherret
Copy link
Contributor

dsherret commented Apr 8, 2019

@jet2jet it's possible to not use any runtime code...

import { Has, AssertTrue } from "conditional-type-checks";

type _doTests = AssertTrue<Has<SomeType, WantedType>>;

...but yeah, that creates the problem with the unused local. To solve that, I think the TypeScript's type checker should probably be changed to not throw an unused local error when the type alias is prefixed with an underscore similar to its behaviour with parameters (alternatively, you could just add an export keyword and never use the export—not ideal).

Conditional types are very flexible for doing many things today. For example, it's possible to check if a type is exactly the same as another type (ex. ensuring type A is exactly the same as type B.... where type A is string | number and it would error when type B is string or even string | number | Date) or see if the type is the any type, unknown type, never type, etc...

Do you think this new syntax should also support using conditional types somehow? Or would it be possible to say that one type should exactly equal another (not just extends)?

@jet2jet
Copy link
Author

jet2jet commented Apr 9, 2019

Do you think this new syntax should also support using conditional types somehow?

Yes, type assert (A extends B ? C : D) extends (P extends Q ? R : T) should be allowed.

Or would it be possible to say that one type should exactly equal another (not just extends)?

I think it would not be possible; when both type assert M extends N and type assert N extends M have no error, M is almost equal to N.
(This is like relation of M and N in case TCheck is true for type TCheck = M extends N ? (N extends M ? true : false) : false .)

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 9, 2019
@dead-claudia
Copy link

This would be invaluable for validating more complicated type-level programming. I'm already commonly doing type _AssertExtends<T extends U, U> = never followed by type _TestN = _AssertExtends<Foo, Bar>. I'm also doing similar with type _Assert<T extends true> = never.

@krryan
Copy link

krryan commented Sep 25, 2024

Definitely want this; I’ve worked around this kind of thing in my code a lot, and the workarounds are a pain.

This most recently came up in discussion of #13298, where I had a couple of suggestions for achieving the same compile-time check that the suggestion was trying to get at: here and here. But both approaches have drawbacks, when what I really want to write is:

export type TheUnion = 'foo' | 'bar';

/** A correct tuple, in the same order as the union */
export const validTuple = ['foo', 'bar'] as const;
assert type typeof validTuple extends SetTuple<typeof validTuple>; // no error
assert type typeof validTuple extends readonly TheUnion[]; // no error
assert type typeof TheUnion extends typeof validTuple[number]; // no error

/** A correct tuple, in the opposite order as the union (doesn't matter) */
export const alsoValidTuple = ['bar', 'foo'] as const;
assert type typeof alsoValidTuple extends SetTuple<typeof alsoValidTuple>; // no error
assert type typeof alsoValidTuple extends readonly TheUnion[]; // no error
assert type TheUnion extends typeof alsoValidTuple[number]; // no error

/** A wrong tuple, for several reasons */
export const invalidTuple = ['bar', 'bar', 'baz'] as const;
type assert typeof invalidTuple extends SetTuple<typeof invalidTuple>;
     ^^^^^^
     Type 'readonly ["bar", "bar", "baz"]' is not assignable to type 'readonly ["bar", "baz"]'.
       Source has 3 element(s) but target allows only 2. ts(2322)
type assert typeof invalidTuple extends readonly TheUnion[];
     ^^^^^^
     Type 'readonly ["bar", "bar", "baz"]' is not assignable to type 'readonly ("foo" | "bar")[]'.
       Type '"bar" | "baz"' is not assignable to type '"foo" | "bar"'.
         Type '"baz"' is not assignable to type '"foo" | "bar"'. ts(2322)
type assert TheUnion extends typeof invalidTuple[number];
     ^^^^^^
     Type '"foo" | "bar"' is not assignable to type '"bar" | "baz"'.
       Type '"foo"' is not assignable to type '"bar" | "baz"'. ts(2322)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants