Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active June 20, 2023 16:37
Show Gist options
  • Save webstrand/5a5df9e63d5646f045a8871f11cb5b6f to your computer and use it in GitHub Desktop.
Save webstrand/5a5df9e63d5646f045a8871f11cb5b6f to your computer and use it in GitHub Desktop.
Examples of the <T>(foo: Validator<T>) pattern in typescript
/******************************************************************************
* Implementation of `Nonempty` validator which checks that the provided type
* has at least one defined property, excluding `{}`.
******************************************************************************/
type Nonempty<T extends { [key: string]: any }> = { [P in keyof T]: T }[keyof T];
declare function wantsNonempty<T extends { [key: string]: any }>(x: Nonempty<T>): true;
wantsNonempty({ x: 1 });
wantsNonempty({}); // error expected
/******************************************************************************
* Implementation of `NonUnion` validator which checks that the provided type
* is not a union. Does not check for primitive types such as `string` or
* `number`.
******************************************************************************/
type NonUnion<T, U extends T = T> =
(T extends T ? U extends T ? 0 : 1 : never) extends 0 ? T : never;
declare function wantsNonUnion<T extends "foo" | "bar" | "baz">(
type: NonUnion<T>
): { [P in T]: unknown };
wantsNonUnion("foo");
wantsNonUnion("bar");
wantsNonUnion("foo" as "foo" | "bar"); //error
// Note that this example can also be solved by generating an overloaded
// function type with overloads for each of "foo" | "bar" | ...
// The overloads provide better code-completion suggestions for users and
// the error when misusing a union type is not opaque.
/******************************************************************************
* Implementation of `Literal` validator which checks that the provided type is
* a single, definite, literal type.
******************************************************************************/
type Literal<T extends string, U extends T = T> = {} extends { [P in T]: unknown } ? never :
(T extends T ? U extends T ? 0 : 1 : never) extends 0 ? T : AnyOf<U>;
type IsTemplateLiteral<T extends string> = {} extends { [P in T]: unknown } ? true : false;
type AnyOf<U> =
(U extends U ? (x: () => U) => 0 : never) extends (x: infer I) => 0
? I extends () => infer R
? Extract<U, R>
: never
: never;
declare function acceptLiteral<T extends string>(x: Literal<T>): void;
declare const a: "foo";
declare const b: "foo" | "bar";
declare const c: string;
declare const d: `foo${string}`;
acceptLiteral(a)
acceptLiteral(b) // Argument of type '"foo" | "bar"' is not assignable to parameter of type '"bar"'.
acceptLiteral(c) // Argument of type 'string' is not assignable to parameter of type 'never'.
acceptLiteral(d); // Argument of type '`foo${string}`' is not assignable to parameter of type 'never'.
/******************************************************************************
* Implementation of AnyOf validator that forces non-union generics by picking
* one value from the union.
******************************************************************************/
type AnyOf<U> =
(U extends U ? (x: () => U) => 0 : never) extends (x: infer I) => 0
? I extends () => infer R
? U extends R ? U : never
: never
: never;
declare function wantsAnyOf<T>(x: AnyOf<T>): void;
wantsAnyOf(1);
wantsAnyOf(2);
wantsAnyOf({} as 1 | 2 | 3); // error
/******************************************************************************
* Implementation of `Exactly` validator which checks that the provided type
* does not contain properties in excess of some base type.
******************************************************************************/
type Base = {
foo: number;
bar: number;
};
// Base is passed through a conditional to split apart union types, otherwise
// exactly breaks on union types.
type Exactly<Base extends object, T extends Base> = Base extends Base ? T & { [P in Exclude<keyof T, keyof Base>]?: never } : never;
declare function fn<T extends Base>(p: Exactly<Base, T>): void;
fn({ foo: 1, bar: 2 });
fn({ foo: 1, bar: 2, baz: 3 }); // error expected
/******************************************************************************
* Validates that a parameter is a literal integer without fractional
* component.
******************************************************************************/
type IsInteger<T extends number | bigint> =
`${T}` extends `${bigint}` | `${bigint}e+${bigint}`
? T
: { error: `${T} is not an integer` };
declare function getOffset<T extends number | bigint>(i: IsInteger<T>): void;
getOffset(1);
getOffset(5.5); // error
getOffset(-7);
getOffset(1e308);
getOffset(Infinity); //error
getOffset(NaN); //error
getIndex(1n);
/**
* Only permits natural numbers, i.e. positive integers.
*/
type IsNatural<T extends number | bigint> =
`-${T}` extends `${bigint}` | `${bigint}e+${bigint}`
? T
: { error: `${T} is not a natural number (zero inclusive)` };
declare function getIndex<T extends number | bigint>(i: IsNatural<T>): void;
getIndex(-0) // unfixable :(
getIndex(1);
getIndex(5.5); // error
getIndex(-7); // error
getIndex(1e308);
getIndex(1e309); //error
getIndex(Infinity); //error
getIndex(NaN); //error
getIndex(1n);
/******************************************************************************
* Implementation of `PipeValidate` validator which checks that the provided array
* type contains a sequence of functions where for each member, the argument
* type matches the previous return type.
******************************************************************************/
type Pipeable<P = never, R extends unknown = unknown> = (arg: P) => R;
/**
* A sequence of Pipeable functions. May be invalid, check with
* PipeValidate<T>.
*/
type Pipe<T extends readonly Pipeable[] = readonly Pipeable[]> = T;
/**
* Return T matching the mutability of Z. For example if Z is readonly
* so will the produced type, and if Z is not readonly, neither will the
* produced type.
*/
type MatchMutability<T extends readonly unknown[], Z extends readonly unknown[]> =
Z extends unknown[] ? [...T] : readonly [...T];
/**
* Replace the argument of a pipeable function without changing its name.
*/
type ReplaceArg<T extends Pipeable, Replacement> =
T extends (...args: infer U) => infer V
? (...args: { [P in keyof U]: P extends "0" ? Replacement : U[P] }) => V
: (arg: Replacement) => unknown;
/**
* Validate that every member of some Pipe is compatible with the next.
*/
type PipeValidate<T extends Pipe> =
T extends readonly [Pipeable<never, infer NextArg>, ...Pipe<infer P>]
? _PipeValidate<NextArg, P, [T[0]], T>
: MatchMutability<[Pipeable, ...Pipeable[]], T>
/** @private */
type _PipeValidate<Arg, T extends Pipe, A extends Pipe, Z extends Pipe> =
T extends readonly [Pipeable<Arg, infer NextArg>, ...Pipe<infer P>]
? _PipeValidate<NextArg, P, [...A, T[0]], Z>
: T extends readonly []
? Z
: MatchMutability<[...A, ReplaceArg<T[0], Arg>, ...Pipeable[]], Z>;
/**
* Obtain the return type of some pipe sequence.
*/
type PipeReturnType<T extends Pipe> =
T extends readonly [...Pipe, Pipeable<never, infer ReturnType>]
? ReturnType
: unknown;
declare function foo(a: string): number;
declare function bar(a: number): symbol;
declare function baz(a: symbol): string[];
declare function qux<T>(a: T): T;
declare function pipe<T extends Pipe>(...funcs: PipeValidate<T>): (...args: Parameters<T[0]>) => PipeReturnType<T>;
pipe(foo, bar);
pipe(foo, bar, baz);
pipe(foo, (x: number) => "");
pipe(foo, baz, baz); // error
pipe(foo, (x) => x) // error, user must specify types
pipe(foo, qux, bar); // error, generic functions aren't supported.
// Thanks to @keithlayne; a hack to workaround generic functions by fixing their
// generic arguments beforehand.
function fix<P, R extends unknown>(fn: Pipeable<P, R>) { return fn }
pipe(foo, fix<number, number>(qux), bar);
/******************************************************************************
* Implementation of `IsUnaryFunction` validator which requires that the
* provided function type has exactly one argument.
******************************************************************************/
type IsUnaryFunction<T extends (...args: any) => any> = Parameters<T> extends [] ? "function must accept exactly one argument" : T;
declare function callUnaryFunction<T extends (...args: any) => any>(fn: IsUnaryFunction<T>): void;
callUnaryFunction(() => {}); // error expected
callUnaryFunction((v: string) => {}); // no error
/******************************************************************************
* Implementation of `covary` validator which requires that the provided record
* contains property names that match their value's name property value.
******************************************************************************/
declare function covary<T extends Record<string, { name: string }>>(map: T & { [P in keyof T]: { name: P } }): void;
covary({}); // no error
covary({ foo: { name: "foo" }, bar: { name: "bar" } }); // no error
covary({ foo: { name: "bar" }, bar: { name: "foo" } }); // error expected
/******************************************************************************
* Implementation of `NoUndefined` validator which requires that every property
* be defined. Useful for preventing users from passing objects with declared
* but undefined properties. Can't be relied upon, though. There are many ways
* to accidentally bypass this validator.
******************************************************************************/
type SomeOptionalType = { a?: string, b?: number, c: string };
type NoUndefined<T> = { [P in keyof T]: T[P] extends undefined ? never : T[P] }
declare function acceptNoUndefined<T extends SomeOptionalType>(prop: NoUndefined<T>): void;
acceptNoUndefined({}) // invalid
acceptNoUndefined({ c: "" }) // valid
acceptNoUndefined({ b: undefined, c: "" }); // invalid
acceptNoUndefined({ a: "", c: ""}); // valid
/******************************************************************************
* Validates that a string is a positive decimal of some kind. Does not accept
* shorthand ".5" or "5." notation.
******************************************************************************/
type ValidatePositiveDecimal<N extends `${number}`, Pass = N> =
N extends `${infer U}.${infer V}`
? `-${U}.-${V}` extends `${bigint}.${bigint}` ? Pass : never
: `-${N}` extends `${bigint}` ? Pass : never;
declare function dec<N extends `${number}`>(x: ValidatePositiveDecimal<N>): void;
dec("0");
dec("5");
dec("5000");
dec("55.55");
// errors
dec(".5");
dec("5.");
dec("-5");
dec("-5.-5");
dec("5.-5");
dec("1e32");
dec("-1e22");
dec("0.3e-22");
dec("Infinity");
dec("NaN");
/******************************************************************************
* Validates that a string is a positive decimal followed by some unit of
* length.
******************************************************************************/
type Units = 'px'|'ch'|'pt'|'cm'|'in'|'em'|'';
type Length = `${number}${` ${Exclude<Units, "">}` | `${Units}`}`;
type ValidateLength<L extends Length, Pass = L> =
L extends `${infer N}${` ${Exclude<Units, "">}` | `${Units}`}`
? ValidatePositiveDecimal<N & `${number}`, Pass>
: never;
declare function len<L extends Length>(x: ValidateLength<L>): void;
len("0em");
len("5");
len("5000cm");
len("55.55 pt");
// errors
len(".5");
len("5.");
len("-5 in");
len("-5.-5");
len("5.-5");
len("1e32");
len("-1e22");
len("0.3e-22");
len("Infinity ch");
len("NaN");
/******************************************************************************
* Implementation of a validator that only accepts tuple types that contain
* every member of some union. Excluding duplicates and unknown types.
******************************************************************************/
type CompleteTupleOf<K, T extends readonly unknown[], R extends readonly unknown[] = readonly[], TT extends readonly unknown[] = T> =
T extends unknown
// check if one or more elements remaining in the tuple?
? T extends readonly [infer X extends K, ...infer Y] & { 0: infer XX }
? (X extends X ? XX extends X ? 0 : 1 : never) extends 0
? CompleteTupleOf<Exclude<K, X>, Y, readonly [...R, X], TT>
: readonly [...R, "error: must not be union", ...K[]] & { invalid: true }
// check if the union is empty?
: [K] extends [never]
// check if there are no excess keys
? T extends readonly[]
// We must reuse the original binding or inference fails
? TT
// Prevent excess keys
: R & { invalid: true }
// union has unsatisfied elements remaining
// we intersect `{ invalid: true }` to force unassignability in the face of wildcards like `string` or `number`
: readonly [...R, K] & { invalid: true } // K hasn't been emptied, generate an error
: T; // provide inference site, unreachable
// test cases:
type Union = "a" | "b";
declare function completeTuple<T extends Union[]>(accept: CompleteTupleOf<Union, T>): void;
completeTuple(null! as Union[]); // error: non-tuples
completeTuple(null! as [Union]); // error: elements must be literals
completeTuple([]) // error: missing a, b
completeTuple(["a"]); // error: missing b
completeTuple(["b"]); // error: missing a
completeTuple(["a", "b"]);
completeTuple(["b", "a"]);
completeTuple(["b", "b", "a"]); // error: excess key
completeTuple(["b", "a", "a"]); // error: excess key
completeTuple(["b", "a", "c"]); // error: unknown excess key
// Example Usage:
// Typescript can't guess what keys `Object.keys` or `foo in obj`
// return at runtime, since typescript only guarantees that know keys exist
// but there may be arbitrary unknown (excess) properties on an object.
// Thus it types both of the above as `string[]`.
//
// This causes issues when you're trying to iterate over the keys of an object but
// typescript won't stop complaining. You can use `CompleteTupleOf` to fix this:
function keyof<T extends {}, K extends readonly [] | readonly ((string & {}) | "" | (number & {}) | 0 | (symbol & {}))[]>(o: T, keys: CompleteTupleOf<keyof T, K>): K {
return keys as K
}
interface Example {
foo: string;
"baz": string;
111: string;
}
declare const example: Example;
for(const k of keyof(example, ["baz", "foo", 111])) {
example[k] // no error
}
declare const myObj: { x: number, y: string };
for(const key of keyof(myObj, ["x", "y"])) {
console.log(myObj[key]); // is type-safe and works without error.
}
// If you add or remove a key from the object type, the compiler will report an error:
for(const key of keyof(myObj, ["x", "y", "z"])) {}
for(const key of keyof(myObj, ["x"])) {}
/******************************************************************************
* Implementation of `NoDuplicateElements` validator that checks that a tuple
* type has no duplicated elements; that every element is distinct at the type
* level.
******************************************************************************/
type NoDuplicateElements<T extends readonly unknown[], A extends unknown[] = [], Z extends readonly unknown[] = T> =
T extends readonly [infer X, ...infer Y]
? X extends Y[number]
? [...A, X]
: NoDuplicateElements<Y, [...A, X], Z>
: Z;
declare function uniqueArray<T extends unknown[] | []>(accept: NoDuplicateElements<T>): void;
declare const a: 1;
declare const b: 2;
declare const c: 3;
uniqueArray([]);
uniqueArray([a]);
uniqueArray([a,b]);
uniqueArray([b,a]);
uniqueArray([a,a]); // Argument of type '[1, 1]' is not assignable to parameter of type '[1]'.
uniqueArray([b,b]); // Argument of type '[2, 2]' is not assignable to parameter of type '[2]'.
/******************************************************************************
* Implementation of a validator for SQL sort strings. Very simple, and
* whitespace-sensitive. But it handles auto-completion well and provides
* suggestions.
******************************************************************************/
type Sort<T> =
| `${string & keyof T}`
| `${string & keyof T} asc`
| `${string & keyof T} ASC`
| `${string & keyof T} desc`
| `${string & keyof T} DESC`;
type ValidateSort<T, U extends string> =
U extends `${Sort<T>}${infer V}` ? V extends "" ? U : _ValidateSort<T, V, U extends `${infer W}${V}` ? W : never, U> : Sort<T>;
type _ValidateSort<T, U extends string, P extends string, Z extends string> =
U extends `, ${Sort<T>}${infer V}` ? V extends "" ? Z : _ValidateSort<T, V, U extends `${infer W}${V}` ? `${P}${W}` : never, Z> : `${P}, ${Sort<T>}`;
declare class Sortable<T> {
sort<U extends string>(by: ValidateSort<T, U>): void;
}
declare class SomeEntity {
id: unknown;
name: unknown;
}
const sortable = new Sortable<SomeEntity>();
sortable.sort('name');
sortable.sort('name desc, id');
sortable.sort('name desc, id, foo'); // invalid
/******************************************************************************
* Validates parameter names in strings like "foo #{bar} #{baz}". Also provides
* autocomplete.
******************************************************************************/
type ValidateParams<T extends string, K extends string> =
T extends `${infer U}#{${infer V}`
? V extends `${infer W}}${infer X}`
? W extends K
? `${U}#{${W}}${ValidateParams<X, K>}`
: `${U}#{${K}}${ValidateParams<X, K>}`
: `${U}#{${K}}`
: T;
declare class Log {
set<T>(params: T): {
message<U extends string>(msg: ValidateParams<U, keyof T & string>): void
}
}
declare const log: Log;
log.set({
post_id: 123,
other_prop: 1
}).message('test #{post_id} #{other_prop}');
// type `p` or `o` to start auto-complete, <tab> to select
/******************************************************************************
* Better inference short-circuiting. Will always infer T, but will instantiate
* to "foo".
******************************************************************************/
type FastInfer<T> = [T,T|1] extends [never,2] ? T : "foo";
import { Exact } from "./check"; // https://gist.github.com/webstrand/b0f79ef6ed37839d1432466fe8ddbc1a
type StringKeys<T> =
T extends T
? | `${keyof T & string}`
| &`${keyof T & number}` & `${bigint}` // "1e0" does not extend `${number}` & `${bigint}`
& ([T] extends [readonly unknown[]] ? 1e309 extends T["length"] ? unknown : keyof T : unknown) // disable `${number}` on finite tuples
: never;
// Cannot be unified with StringKeys because `${P}${StringKeys<T>}` breaks in certain edge cases.
type CandidateStringKeys<T, P extends string> =
T extends T
? | `${P}${keyof T & string}`
| & `${P}${keyof T & number}` & `${P}${bigint}` // "1e0" does not extend `${number}` & `${bigint}`
& ([T] extends [readonly unknown[]] ? 1e309 extends T["length"] ? unknown : /* is finite */ never : unknown) // disable `${number}` on finite tuples
: never;
type PathOf<T, K extends string, P extends string = ""> =
K extends `${infer U}.${infer V}`
? U extends StringKeys<T>
? T extends T ? PathOf<T[U & keyof T], V, `${P}${U}.`> : never
: CandidateStringKeys<T, P>
: ("" extends K ? unknown : K) extends StringKeys<T> // If K = "" we need to avoid matching `${string}`
? `${P}${K}`
: CandidateStringKeys<T, P>;
// TypeScript does not like "0" extends keyof { 0: ... }, so we have to write out own descend
type Descend<T, K extends string> =
T extends { [P in K]?: infer X }
? X | (T extends { [P in K]: any } ? never : undefined)
: T[K & keyof T] | undefined; // index access type of T is to support indexed types
type ResolvePath<T, K extends string> =
K extends `${infer U}.${infer V}`
? T extends T ? ResolvePath<Descend<T, U>, V>: never
: T extends T ? Descend<T, K> : never;
////////////////////////////////////////////////////////////////////////////////
// How to use:
////////////////////////////////////////////////////////////////////////////////
declare const o: Foo;
declare function select<O, K extends string>(o: O, x: PathOf<O, K>): ResolvePath<O, K>;
select(o, "y");
select(o, "z");
select(o, "0");
select(o, "method");
select(o, "w.n.1");
select(o, "x.z.b.1.0");
////////////////////////////////////////////////////////////////////////////////
// Test cases:
////////////////////////////////////////////////////////////////////////////////
type Foo = {
w: Bar;
x: Foo;
y?: Date;
z: Baz;
0: boolean;
method(): void;
};
type Bar = { m: Bar.M } | { n: Bar.N } | { o: Bar.O };
declare namespace Bar {
type M = { 0: string };
type N = { 1: string };
type O = "o";
}
type HMT<T> = {
[P in keyof T]: P extends "prime" ? string : Foo
};
type Baz = {
a: [];
b: [1,Foo,"3",4];
c: [1,Foo,"3",4,...string[]];
d: Foo[];
e: { prime: never, [key: string]: Foo };
f: { prime: string, [key: number]: Foo };
g: HMT<{ prime: never, [key: string]: never }>;
};
// root
Exact<PathOf<Foo, "x">, "x">(true);
Exact<PathOf<Foo, "y">, "y">(true);
Exact<PathOf<Foo, "z">, "z">(true);
Exact<PathOf<Foo, "0">, "0">(true);
Assignable<"1", PathOf<Foo, "">>(false);
Exact<PathOf<Foo, "method">, "method">(true);
Exact<PathOf<Foo, "">, "w" | "x" | "y" | "z" | "0" | "method">(true);
Exact(select(o, "w"))<Foo["w"]>(true);
Exact(select(o, "x"))<Foo["x"]>(true);
Exact(select(o, "y"))<Foo["y"]>(true);
Exact(select(o, "z"))<Foo["z"]>(true);
Exact(select(o, "0"))<Foo["0"]>(true);
// @ts-expect-error
select(o, "1")
Exact(select(o, "method"))<Foo["method"]>(true);
Exact(select(o, "" as "w" | "x" | "y" | "z"))<Foo["w"] | Foo["x"] | Foo["y"] | Foo["z"]>(true);
Exact(select(o, "" as "w" | "x" | "y" | "z" | "0" | "method"))<Foo["w"] | Foo["x"] | Foo["y"] | Foo["z"] | Foo["0"] | Foo["method"]>(true);
Exact(select(o, "" as `${keyof Foo}`))<Foo[keyof Foo]>(true);
// w
Exact<PathOf<Foo, "w.m">, "w.m">(true);
Exact<PathOf<Foo, "w.n">, "w.n">(true);
Exact<PathOf<Foo, "w.o">, "w.o">(true);
Exact<PathOf<Foo, "w.">, "w.m" | "w.n" | "w.o">(true);
Exact(select(o, "w.m"))<Bar.M | undefined>(true);
Exact(select(o, "w.n"))<Bar.N | undefined>(true);
Exact(select(o, "w.o"))<Bar.O | undefined>(true);
Exact(select(o, "" as "w.m" | "w.n"))<Bar.M | Bar.N | undefined>(true);
Exact(select(o, "" as "w.m" | "w.n" | "w.o"))<Bar.M | Bar.N | Bar.O | undefined>(true);
Exact(select(o, "" as `w.${StringKeys<Bar>}`))<Bar.M | Bar.N | Bar.O | undefined>(true);
// x
Exact<PathOf<Foo, "x.w">, "x.w">(true);
Exact<PathOf<Foo, "x.x">, "x.x">(true);
Exact<PathOf<Foo, "x.y">, "x.y">(true);
Exact<PathOf<Foo, "x.z">, "x.z">(true);
Exact<PathOf<Foo, "x.0">, "x.0">(true);
Assignable<"x.1", PathOf<Foo, "x.">>(false);
Exact<PathOf<Foo, "x.method">, "x.method">(true);
Exact<PathOf<Foo, "x.">, "x.w" | "x.x" | "x.y" | "x.z" | "x.0" | "x.method">(true);
Exact(select(o, "x.w"))<Foo["w"]>(true);
Exact(select(o, "x.x"))<Foo["x"]>(true);
Exact(select(o, "x.y"))<Foo["y"]>(true);
Exact(select(o, "x.z"))<Foo["z"]>(true);
Exact(select(o, "x.0"))<Foo["0"]>(true);
Exact(select(o, "x.method"))<Foo["method"]>(true);
Exact(select(o, "" as "x.w" | "x.x" | "x.y" | "x.z"))<Foo["w"] | Foo["x"] | Foo["y"] | Foo["z"]>(true);
Exact(select(o, "" as "x.w" | "x.x" | "x.y" | "x.z" | "x.0" | "x.method"))<Foo["w"] | Foo["x"] | Foo["y"] | Foo["z"] | Foo["0"] | Foo["method"]>(true);
Exact(select(o, "" as `x.${keyof Foo}`))<Foo[keyof Foo]>(true);
// y
Exact<PathOf<Foo, "y.">, `y.${keyof Date & string}`>(true);
Exact(select(o, "y.getHours"))<Date["getHours"] | undefined>(true);
Exact(select(o, "y.getMinutes"))<Date["getMinutes"] | undefined>(true);
Exact(select(o, "" as "y.getSeconds" | "y.getFullYear"))<Date["getSeconds"] | Date["getFullYear"] | undefined>(true);
Exact(select(o, "" as `y.${keyof Date & string}`))<Date[keyof Date & string] | undefined>(true);
// z
Exact<PathOf<Foo, "z.a">, "z.a">(true);
Exact<PathOf<Foo, "z.b">, "z.b">(true);
Exact<PathOf<Foo, "z.c">, "z.c">(true);
Exact<PathOf<Foo, "z.d">, "z.d">(true);
Exact<PathOf<Foo, "z.e">, "z.e">(true);
Exact<PathOf<Foo, "z.f">, "z.f">(true);
Exact<PathOf<Foo, "z.g">, "z.g">(true);
Exact<PathOf<Foo, "z.">, "z.a" | "z.b" | "z.c" | "z.d" | "z.e" | "z.f" | "z.g">(true);
Exact(select(o, "z.a"))<Baz["a"]>(true);
Exact(select(o, "z.b"))<Baz["b"]>(true);
Exact(select(o, "z.c"))<Baz["c"]>(true);
Exact(select(o, "z.d"))<Baz["d"]>(true);
Exact(select(o, "z.e"))<Baz["e"]>(true);
Exact(select(o, "z.f"))<Baz["f"]>(true);
Exact(select(o, "z.g"))<Baz["g"]>(true);
// @ts-expect-error
select(o, "z.zzzz");
Exact(select(o, "" as "z.a" | "z.b"))<Baz["a"] | Baz["b"]>(true);
Exact(select(o, "" as "z.a" | "z.b" | "z.c" | "z.d" | "z.e" | "z.f" | "z.g"))<Baz["a"] | Baz["b"] | Baz["c"] | Baz["d"] | Baz["e"] | Baz["f"] | Baz["g"]>(true);
Exact(select(o, "" as `z.${keyof Baz}`))<Baz[keyof Baz]>(true);
// z.a
Assignable<"z.a.0", PathOf<Foo, "z.a.0">>(false);
Assignable<"z.a.1", PathOf<Foo, "z.a.1">>(false);
Assignable<"z.a.1e0", PathOf<Foo, "z.a.">>(false);
Exact<PathOf<Foo, "z.a.length">, `z.a.length`>(true);
Exact<PathOf<Foo, "z.a.slice">, `z.a.slice`>(true);
Exact<PathOf<Foo, "z.a.">, `z.a.${keyof Array<any> & string}`>(true);
// @ts-expect-error
select(o, "z.a.0");
// @ts-expect-error
select(o, "z.a.1");
// @ts-expect-error
select(o, "z.a.1e0");
Exact(select(o, "z.a.length"))<Foo["z"]["a"]["length"]>(true);
Exact(select(o, "z.a.slice"))<Foo["z"]["a"]["slice"]>(true);
Exact(select(o, "" as "z.a.slice" | "z.a.length"))<Foo["z"]["a"]["slice"] | Foo["z"]["a"]["length"]>(true);
Exact(select(o, "" as `z.a.${keyof Array<any> & string}`))<Foo["z"]["a"][keyof Foo["z"]["a"] & string]>(true);
// z.b
Exact<PathOf<Foo, "z.b.0">, "z.b.0">(true);
Exact<PathOf<Foo, "z.b.1">, "z.b.1">(true);
Exact<PathOf<Foo, "z.b.2">, "z.b.2">(true);
Exact<PathOf<Foo, "z.b.3">, "z.b.3">(true);
Assignable<"z.b.4", PathOf<Foo, "z.b.4">>(false);
Assignable<"z.b.1e0", PathOf<Foo, "z.b.">>(false);
Exact<PathOf<Foo, "z.b.">, "z.b.0" | "z.b.1" | "z.b.2" | "z.b.3" | `z.b.${keyof Array<any> & string}`>(true);
Exact(select(o, "z.b.0"))<Foo["z"]["b"][0]>(true);
Exact(select(o, "z.b.1"))<Foo["z"]["b"][1]>(true);
Exact(select(o, "z.b.2"))<Foo["z"]["b"][2]>(true);
Exact(select(o, "z.b.3"))<Foo["z"]["b"][3]>(true);
// @ts-expect-error
select(o, "z.b.4");
Exact(select(o, "z.b.length"))<Foo["z"]["b"]["length"]>(true);
Exact(select(o, "z.b.slice"))<Foo["z"]["b"]["slice"]>(true);
Exact(select(o, "" as "z.b.slice" | "z.b.length"))<Foo["z"]["b"]["slice"] | Foo["z"]["b"]["length"]>(true);
Exact(select(o, "" as `z.b.${keyof Foo["z"]["b"] & string}`))<Foo["z"]["b"][keyof Foo["z"]["b"] & string]>(true);
// z.c
Exact<PathOf<Foo, "z.c">, "z.c">(true);
Exact<PathOf<Foo, "z.c.0">, "z.c.0">(true);
Exact<PathOf<Foo, "z.c.1">, "z.c.1">(true);
Exact<PathOf<Foo, "z.c.2">, "z.c.2">(true);
Exact<PathOf<Foo, "z.c.3">, "z.c.3">(true);
Exact<PathOf<Foo, "z.c.99999">, "z.c.99999">(true);
Assignable<"z.c.1e0", PathOf<Foo, "z.c.">>(false);
Exact<PathOf<Foo, "z.c.">,"z.c.0" | "z.c.1" | "z.c.2" | "z.c.3" | `z.c.${keyof Array<any> & string}` | `z.c.${number}` & `z.c.${bigint}`>(true);
Exact(select(o, "z.c.0"))<Foo["z"]["c"][0]>(true);
Exact(select(o, "z.c.1"))<Foo["z"]["c"][1]>(true);
Exact(select(o, "z.c.2"))<Foo["z"]["c"][2]>(true);
Exact(select(o, "z.c.3"))<Foo["z"]["c"][3]>(true);
Exact(select(o, "z.c.4"))<Foo["z"]["c"][number | "0" | "1" | "2" | "3"] | undefined>(true); // less than ideal, but sufficient.
Exact(select(o, "z.c.length"))<Foo["z"]["c"]["length"]>(true);
Exact(select(o, "z.c.slice"))<Foo["z"]["c"]["slice"]>(true);
Exact(select(o, "" as "z.c.slice" | "z.c.length"))<Foo["z"]["c"]["slice"] | Foo["z"]["c"]["length"]>(true);
Exact(select(o, "" as `z.c.${keyof Foo["z"]["c"] & string}`))<Foo["z"]["c"][keyof Foo["z"]["c"] & string]>(true);
// z.d
Exact<PathOf<Foo, "z.d">, "z.d">(true);
Exact<PathOf<Foo, "z.d.0">, "z.d.0">(true);
Exact<PathOf<Foo, "z.d.1">, "z.d.1">(true);
Exact<PathOf<Foo, "z.d.99999">, "z.d.99999">(true);
Assignable<"z.d.1e0", PathOf<Foo, "z.d.">>(false);
Exact<PathOf<Foo, "z.d.">, `z.d.${keyof Array<any> & string}` | `z.d.${number}` & `z.d.${bigint}`>(true);
Exact(select(o, "z.d.0"))<Foo["z"]["d"][0] | undefined>(true);
Exact(select(o, "z.d.1"))<Foo["z"]["d"][1] | undefined>(true);
Exact(select(o, "z.d.2"))<Foo["z"]["d"][2] | undefined>(true);
Exact(select(o, "z.d.99999"))<Foo["z"]["d"][99999] | undefined>(true);
Exact(select(o, "z.d.length"))<Foo["z"]["d"]["length"]>(true);
Exact(select(o, "z.d.slice"))<Foo["z"]["d"]["slice"]>(true);
Exact(select(o, "" as "z.d.slice" | "z.d.length"))<Foo["z"]["d"]["slice"] | Foo["z"]["d"]["length"]>(true);
Exact(select(o, "" as `z.d.${keyof Foo["z"]["d"] & string}`))<Foo["z"]["d"][keyof Foo["z"]["d"] & string]>(true);
// z.e
Exact<PathOf<Foo, "z.e">, "z.e">(true);
Exact<PathOf<Foo, "z.e.prime">, "z.e.prime">(true);
Exact<PathOf<Foo, "z.e.0">, "z.e.0">(true);
Exact<PathOf<Foo, "z.e.1">, "z.e.1">(true);
Exact<PathOf<Foo, "z.e.99999">, "z.e.99999">(true);
Exact<PathOf<Foo, "z.e.foo">, "z.e.foo">(true);
Exact<PathOf<Foo, "z.e.1e0">, "z.e.1e0">(true);
Exact<PathOf<Foo, "z.e.">, `z.e.${string}` | (`z.e.${number}` & `z.e.${bigint}`)>(true);
Exact(select(o, "z.e.prime"), "" as never, true);
Exact(select(o, "z.e.0"))<Foo["z"]["e"][0] | undefined>(true);
Exact(select(o, "z.e.1"))<Foo["z"]["e"][1] | undefined>(true);
Exact(select(o, "z.e.2"))<Foo["z"]["e"][2] | undefined>(true);
Exact(select(o, "z.e.1e1"))<Foo["z"]["e"][1e1] | undefined>(true);
Exact(select(o, "z.e.99999"))<Foo["z"]["e"][99999] | undefined>(true);
Exact(select(o, "z.e.foo"))<Foo["z"]["e"]["foo"] | undefined>(true);
Exact(select(o, "z.e.1e0"))<Foo["z"]["e"]["1e0"] | undefined>(true);
// broken until 4.3
// Exact(select(o, "" as `z.e.${keyof Foo["z"]["e"] & string}`))<Foo["z"]["e"][keyof Foo["z"]["e"] & string]>(true);
// z.f
Exact<PathOf<Foo, "z.f">, "z.f">(true);
Exact<PathOf<Foo, "z.f.prime">, "z.f.prime">(true);
Exact<PathOf<Foo, "z.f.0">, "z.f.0">(true);
Exact<PathOf<Foo, "z.f.1">, "z.f.1">(true);
Exact<PathOf<Foo, "z.f.99999">, "z.f.99999">(true);
Assignable<"z.f.foo", PathOf<Foo, "z.f.foo">>(false);
Exact<PathOf<Foo, "z.f.">, `z.f.prime` | (`z.f.${number}` & `z.f.${bigint}`)>(true);
Exact(select(o, "z.f.prime"))<Foo["z"]["f"]["prime"]>(true);
Exact(select(o, "z.f.prime.length"))<Foo["z"]["f"]["prime"]["length"]>(true);
Exact(select(o, "z.f.0"))<Foo["z"]["f"][0] | undefined>(true);
Exact(select(o, "z.f.1"))<Foo["z"]["f"][1] | undefined>(true);
Exact(select(o, "z.f.2"))<Foo["z"]["f"][2] | undefined>(true);
Exact(select(o, "z.f.99999"))<Foo["z"]["f"][99999] | undefined>(true);
// @ts-expect-error
select(o, "z.f.foo");
Exact(select(o, "" as `z.f.${keyof Foo["z"]["f"] & string}`))<Foo["z"]["f"][keyof Foo["z"]["f"] & string]>(true);
// z.g
Exact<PathOf<Foo, "z.g">, "z.g">(true);
Exact<PathOf<Foo, "z.g.prime">, "z.g.prime">(true);
Exact<PathOf<Foo, "z.g.prime.length">, "z.g.prime.length">(true);
Exact<PathOf<Foo, "z.g.0">, "z.g.0">(true);
Exact<PathOf<Foo, "z.g.1">, "z.g.1">(true);
Exact<PathOf<Foo, "z.g.99999">, "z.g.99999">(true);
Exact<PathOf<Foo, "z.g.foo">, "z.g.foo">(true);
Exact<PathOf<Foo, "z.g.1e0">, "z.g.1e0">(true);
Exact<PathOf<Foo, "z.g.">, `z.g.${string}` | (`z.g.${number}` & `z.g.${bigint}`)>(true);
Exact(select(o, "z.g.prime"))<Foo["z"]["g"]["prime"]>(true);
Exact(select(o, "z.g.prime.length"))<Foo["z"]["g"]["prime"]["length"]>(true);
Exact(select(o, "z.g.0"))<Foo["z"]["g"][0] | undefined>(true);
Exact(select(o, "z.g.1"))<Foo["z"]["g"][1] | undefined>(true);
Exact(select(o, "z.g.2"))<Foo["z"]["g"][2] | undefined>(true);
Exact(select(o, "z.g.1e1"))<Foo["z"]["g"][1e1] | undefined>(true);
Exact(select(o, "z.g.99999"))<Foo["z"]["g"][99999] | undefined>(true);
Exact(select(o, "z.g.foo"))<Foo["z"]["g"]["foo"] | undefined>(true);
Exact(select(o, "z.g.1e0"))<Foo["z"]["g"]["1e0"] | undefined>(true);
// broken until 4.3
// Exact(select(o, "" as `z.g.${keyof Foo["z"]["g"] & string}`))<Foo["z"]["g"][keyof Foo["z"]["g"] & string]>(true);
// z.a.*
// has no elements
// z.b.*
Exact<PathOf<Foo, "z.b.0">, "z.b.0">(true);
Exact<PathOf<Foo, "z.b.0.toString">, `z.b.0.toString`>(true);
Assignable<`z.b.0.foo`, PathOf<Foo, "z.b.0.foo">>(false);
Exact<PathOf<Foo, "z.b.0.">, `z.b.0.${keyof Number & string}`>(true);
Exact<PathOf<Foo, "z.b.1">, "z.b.1">(true);
Exact<PathOf<Foo, "z.b.1.x.y">, "z.b.1.x.y">(true);
Exact<PathOf<Foo, "z.b.1.">, "z.b.1.w" | "z.b.1.x" | "z.b.1.y" | "z.b.1.z" | "z.b.1.0" | "z.b.1.method">(true);
Exact<PathOf<Foo, "z.b.2">, "z.b.2">(true);
Exact<PathOf<Foo, "z.b.2.trim">, "z.b.2.trim">(true);
Exact<PathOf<Foo, "z.b.2.">, `z.b.2.${keyof String & string}` | `z.b.2.${number}` & `z.b.2.${bigint}`>(true);
Assignable<"z.b.4.toString", PathOf<Foo, "z.b.4.toString">>(false);
// z.c.*
Exact<PathOf<Foo, "z.c.0">, "z.c.0">(true);
Exact<PathOf<Foo, "z.c.0.toString">, "z.c.0.toString">(true);
Exact<PathOf<Foo, "z.c.0.">, `z.c.0.${keyof Number & string}`>(true);
Exact<PathOf<Foo, "z.c.1">, "z.c.1">(true);
Exact<PathOf<Foo, "z.c.1.x.y">, "z.c.1.x.y">(true);
Exact<PathOf<Foo, "z.c.1.">, "z.c.1.w" | "z.c.1.x" | "z.c.1.y" | "z.c.1.z" | "z.c.1.0" | "z.c.1.method">(true);
Exact<PathOf<Foo, "z.c.2">, "z.c.2">(true);
Exact<PathOf<Foo, "z.c.2.trim">, "z.c.2.trim">(true);
Exact<PathOf<Foo, "z.c.2.">, `z.c.2.${keyof String & string}` | `z.c.2.${number}` & `z.c.2.${bigint}`>(true);
Exact<PathOf<Foo, "z.c.4">, "z.c.4">(true);
Exact<PathOf<Foo, "z.c.4.length">, "z.c.4.length">(true);
ExactL<PathOf<Foo, "z.c.4.">,
| ("z.c.4.w" | "z.c.4.x" | "z.c.4.y" | "z.c.4.z" | "z.c.4.0" | "z.c.4.method")
| `z.c.4.${keyof Number & string}`
| `z.c.4.${keyof String & string}`
| `z.c.4.${number}` & `z.c.4.${bigint}`
>(true); // less than ideal, but sufficient.
// z.d.*
Exact<PathOf<Foo, "z.d.0">, "z.d.0">(true);
Assignable<"z.d.0.foo.y", PathOf<Foo, "z.d.0.foo.y">>(false);
Exact<PathOf<Foo, "z.d.0.x.y">, "z.d.0.x.y">(true);
Exact<PathOf<Foo, "z.d.0.">, "z.d.0.w" | "z.d.0.x" | "z.d.0.y" | "z.d.0.z" | "z.d.0.0" | "z.d.0.method">(true);
Exact<PathOf<Foo, "z.d.1">, "z.d.1">(true);
Exact<PathOf<Foo, "z.d.1.x.y">, "z.d.1.x.y">(true);
Exact<PathOf<Foo, "z.d.1.">, "z.d.1.w" | "z.d.1.x" | "z.d.1.y" | "z.d.1.z" | "z.d.1.0" | "z.d.1.method">(true);
Exact<PathOf<Foo, "z.d.99999">, "z.d.99999">(true);
Exact<PathOf<Foo, "z.d.99999.x.y">, "z.d.99999.x.y">(true);
Exact<PathOf<Foo, "z.d.99999.">, "z.d.99999.w" | "z.d.99999.x" | "z.d.99999.y" | "z.d.99999.z" | "z.d.99999.0" | "z.d.99999.method">(true);
// z.e.*
Exact<PathOf<Foo, "z.e.prime">, "z.e.prime">(true);
Assignable<"z.e.prime.length", PathOf<Foo, "z.e.prime.length">>(false);
Exact<PathOf<Foo, "z.e.prime.">, never>(true); // booooo
Exact<PathOf<Foo, "z.e.0">, "z.e.0">(true);
Exact<PathOf<Foo, "z.e.0.x.y">, "z.e.0.x.y">(true);
Exact<PathOf<Foo, "z.e.0.">, "z.e.0.w" | "z.e.0.x" | "z.e.0.y" | "z.e.0.z" | "z.e.0.0" | "z.e.0.method">(true);
Exact<PathOf<Foo, "z.e.1e1">, "z.e.1e1">(true);
Exact<PathOf<Foo, "z.e.1e1.x.y">, "z.e.1e1.x.y">(true);
Exact<PathOf<Foo, "z.e.1e1.">,"z.e.1e1.w" | "z.e.1e1.x" | "z.e.1e1.y" | "z.e.1e1.z" | "z.e.1e1.0" | "z.e.1e1.method">(true);
Exact<PathOf<Foo, "z.e.foo">, "z.e.foo">(true);
Exact<PathOf<Foo, "z.e.foo.x.y">, "z.e.foo.x.y">(true);
Exact<PathOf<Foo, "z.e.foo.">, "z.e.foo.w" | "z.e.foo.x" | "z.e.foo.y" | "z.e.foo.z" | "z.e.foo.0" | "z.e.foo.method">(true);
// z.f.*
Exact<PathOf<Foo, "z.f.prime">, "z.f.prime">(true);
Exact<PathOf<Foo, "z.f.prime.length">, "z.f.prime.length">(true);
Assignable<"z.f.prime.foo", PathOf<Foo, "z.f.prime.foo">>(false);
Exact<PathOf<Foo, "z.f.prime.">, `z.f.prime.${keyof string & string}` | `z.f.prime.${number}` & `z.f.prime.${bigint}`>(true);
Exact<PathOf<Foo, "z.f.0">, "z.f.0">(true);
Exact<PathOf<Foo, "z.f.0.x.y">, "z.f.0.x.y">(true);
Assignable<"z.f.0.foo", PathOf<Foo, "z.f.0.foo">>(false);
ExactL<PathOf<Foo, "z.f.0.">, "z.f.0.w" | "z.f.0.x" | "z.f.0.y" | "z.f.0.z" | "z.f.0.0" | "z.f.0.method">(true);
// z.g.*
Exact<PathOf<Foo, "z.g.prime">, "z.g.prime">(true);
Exact<PathOf<Foo, "z.g.prime.length">, "z.g.prime.length">(true);
Assignable<"z.g.prime.foo", PathOf<Foo, "z.g.prime.foo">>(false);
Exact<PathOf<Foo, "z.f.prime.">, `z.f.prime.${keyof string & string}` | `z.f.prime.${number}` & `z.f.prime.${bigint}`>(true);
Exact<PathOf<Foo, "z.g.foo">, "z.g.foo">(true);
Exact<PathOf<Foo, "z.g.foo.x.y">, "z.g.foo.x.y">(true);
Assignable<"z.g.foo.foo", PathOf<Foo, "z.g.foo.foo">>(false);
Exact<PathOf<Foo, "z.g.foo.">, "z.g.foo.w" | "z.g.foo.x" | "z.g.foo.y" | "z.g.foo.z" | "z.g.foo.0" | "z.g.foo.method">(true)
// A type validator that removes excess properties to prevent excess properties in a
// literal assigned to a generic. Does _not_ work for non-object literal assignment,
// since the excess properties are simply removed.
type ExactLiteral<Basis, T> = T extends Function ? T : {
[P in keyof T as P extends keyof Basis ? P : never]: Exact<Basis[P & keyof Basis], T[P]>
};
type PseudoDateString = `${bigint}${bigint}${bigint}${number}-${bigint}${bigint}-${bigint}${bigint}`;
declare const DateString: unique symbol;
type DateString = PseudoDateString & { [DateString]: true };
function validateDateString<T extends PseudoDateString>(s: ValidateDateString<T>): s is typeof s & DateString {
return true
}
const x = "2020-04-14";
if(validateDateString(x)) {
const y: DateString = x // perfect
}
// IMPLEMENTATION:
type DateDigits<Length extends 30 | 31> = {
"0": "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9",
"1": "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9",
"2": "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9",
"3": 31 extends Length ? "0" | "1" : "0",
};
type MonthDateDigits = {
"0": {
"1": DateDigits<31>,
"2": DateDigits<30>, // historical
"3": DateDigits<31>,
"4": DateDigits<30>,
"5": DateDigits<31>,
"6": DateDigits<30>,
"7": DateDigits<31>,
"8": DateDigits<31>,
"9": DateDigits<30>,
},
"1": {
"0": DateDigits<31>,
"1": DateDigits<30>,
"2": DateDigits<31>,
},
};
type ValidateDateString<T0 extends string> =
T0 extends `${infer Y extends `${bigint}${bigint}${bigint}${number}`}-${infer M1 extends `${bigint}`}${infer M2 extends `${bigint}`}-${infer D1 extends `${bigint}`}${infer D2 extends `${bigint}`}`
? Y extends `0${infer YY extends `${bigint}${bigint}${bigint}${number}`}` ? `${YY}-${M1}${M2}-${D1}${D2}`
: M1 extends keyof MonthDateDigits
? M2 extends keyof MonthDateDigits[M1]
? D1 extends keyof MonthDateDigits[M1][M2]
? D2 extends MonthDateDigits[M1][M2][D1]
? T0
: `${Y}-${M1}${M2}-${D1}${MonthDateDigits[M1][M2][D1] & string}`
: `${Y}-${M1}${M2}-${keyof MonthDateDigits[M1][M2] & string}${D2}`
: `${Y}-${M1}${keyof MonthDateDigits[M1] & string}-${D1}${D2}`
: `${Y}-${keyof MonthDateDigits}${M2}-${D1}${D2}`
: PseudoDateString;
// TESTING:
declare function ok<T extends string>(x: ValidateDateString<T>): void;
declare function err<T extends string>(x: ValidateDateString<T> extends T ? never : T): void;
ok("0000-01-01");
ok("0001-01-01");
ok("0001-12-31");
ok("9999-12-31");
ok("99999999-12-31");
ok("10000001-01-01");
// we don't check for incorrect month lengths
ok("1712-02-30"); // this is real
ok("2001-02-29"); // not a leap year
// no invalid days
err("0000-01-00");
err("0000-00-01");
err("0000-13-01");
err("0000-01-32");
err("00000-01-01");
err("00001-01-01");
err("09999-01-01");
err("099999999-12-31");
err("010000001-01-01");
// no ${numbers}
err("099999999.1-12-31");
err("099999999e+1-12-31");
// no excess zero-padding
err("0000-1-01");
err("0000-100-1");
err("0000-010-1");
err("0000-001-1");
err("0000-01-1");
err("0000-01-100");
err("0000-01-010");
err("0000-01-001");
// no incorrect days
err("0000-02-31");
err("0000-11-31");
err("0000-06-31");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment