mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Compile SDK-declared data types, any-valued members and Pick views of classes (#1466)
This commit is contained in:
@@ -6,6 +6,8 @@ export class ClassProtocols {
|
||||
private implemented: Set<ts.Symbol> | null = null;
|
||||
private readonly methods = new Map<string, ts.ClassLikeDeclaration[]>();
|
||||
private readonly answers = new Map<ts.Type, boolean>();
|
||||
/** The program classes each Pick or Omit view accepts (classesAcceptedBy). */
|
||||
private readonly accepted = new Map<ts.Type, ts.Symbol[]>();
|
||||
/** What markIdentityFlows learned about the program, while it runs. */
|
||||
private facts: FlowFacts | null = null;
|
||||
|
||||
@@ -414,6 +416,14 @@ export class ClassProtocols {
|
||||
for (const part of ts.constituentTypes(type)) add(part);
|
||||
return;
|
||||
}
|
||||
// A Pick or Omit view's values are instances of the class it views,
|
||||
// or of any other program class that has the view's members.
|
||||
if (this.pickOfProgramClass(type)) {
|
||||
add(type.getAliasTypeArguments()[0]!);
|
||||
for (const other of this.classesAcceptedBy(type))
|
||||
for (const related of this.classFamily(other, facts)) out.add(related);
|
||||
return;
|
||||
}
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined) return;
|
||||
if (this.isProgramClassInstance(type)) {
|
||||
@@ -429,6 +439,30 @@ export class ClassProtocols {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The program classes with methods whose instances are assignable to
|
||||
* `type`. */
|
||||
private classesAcceptedBy(type: ts.Type): ts.Symbol[] {
|
||||
const known = this.accepted.get(type);
|
||||
if (known !== undefined) return known;
|
||||
this.index();
|
||||
const found = new Set<ts.Symbol>();
|
||||
for (const entries of this.methods.values())
|
||||
for (const declaration of entries) {
|
||||
const symbol = declaration.name
|
||||
? this.checker.getSymbolAtLocation(declaration.name)
|
||||
: undefined;
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
!found.has(symbol) &&
|
||||
this.checker.isTypeAssignableTo(this.checker.getDeclaredTypeOfSymbol(symbol), type)
|
||||
)
|
||||
found.add(symbol);
|
||||
}
|
||||
const result = [...found];
|
||||
this.accepted.set(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A class symbol with its base classes and the program classes that
|
||||
* extend it. */
|
||||
private classFamily(symbol: ts.Symbol, facts: FlowFacts): ts.Symbol[] {
|
||||
@@ -570,6 +604,20 @@ export class ClassProtocols {
|
||||
}
|
||||
}
|
||||
|
||||
/** `Pick<C, K>` or `Omit<C, K>` over a program class instance type: a
|
||||
* view of the class's own members, spelled inline instead of as an
|
||||
* interface. Like an interface the class matches, a view that exposes a
|
||||
* method needs a live receiver (see protocol). */
|
||||
private pickOfProgramClass(type: ts.Type): boolean {
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit"))
|
||||
return false;
|
||||
if (!alias.declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile))
|
||||
return false;
|
||||
const source = type.getAliasTypeArguments()[0];
|
||||
return source !== undefined && this.isProgramClassInstance(source);
|
||||
}
|
||||
|
||||
private isProgramClassInstance(type: ts.Type): boolean {
|
||||
const symbol = type.getSymbol();
|
||||
return (
|
||||
@@ -592,6 +640,8 @@ export class ClassProtocols {
|
||||
return element !== undefined && this.carriesIdentity(element, marked);
|
||||
}
|
||||
if (this.isProgramClassInstance(type)) return true;
|
||||
// A live Pick or Omit view of a program class holds the instance itself.
|
||||
if (this.pickOfProgramClass(type) && this.usesCheckedIdentity(type)) return true;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined) return false;
|
||||
if (marked.has(symbol)) return true;
|
||||
@@ -704,12 +754,13 @@ export class ClassProtocols {
|
||||
const properties = this.checker.getPropertiesOfType(type);
|
||||
if (symbol && protocols.has(symbol)) return true;
|
||||
if (
|
||||
!symbol ||
|
||||
!symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
|
||||
)
|
||||
!this.pickOfProgramClass(type) &&
|
||||
(!symbol ||
|
||||
!symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
|
||||
))
|
||||
)
|
||||
return false;
|
||||
for (const property of properties) {
|
||||
|
||||
@@ -275,10 +275,13 @@ export function genericCallInstance(
|
||||
{
|
||||
const rdecl = rsig ? rsig.declaration : undefined;
|
||||
if (
|
||||
rsig &&
|
||||
rdecl &&
|
||||
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
|
||||
!rdecl.body
|
||||
) {
|
||||
const overload = overloadCallInstance(lowerer, expr, info, rsig, rdecl);
|
||||
if (overload) return overload;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -313,14 +316,7 @@ export function genericCallInstance(
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
lowerer.badType(expr, retTs);
|
||||
}
|
||||
// A body that can return an indexed read that misses returns undefined
|
||||
// there, as in JavaScript, whatever the declared result says. An async
|
||||
// body's promise is always present and settles with undefined there.
|
||||
const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl)
|
||||
? mappedReturn.kind === "promise"
|
||||
? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
|
||||
: lowerer.runtimeOptionalType(mappedReturn)
|
||||
: mappedReturn;
|
||||
const returnType = runtimeReturnType(lowerer, info, mappedReturn);
|
||||
|
||||
// keyof-constrained type parameters (`K extends keyof T`): the bound
|
||||
// LITERAL is semantic — the instance body reads the named field — so
|
||||
@@ -331,23 +327,11 @@ export function genericCallInstance(
|
||||
// share one runtime-keyed instance per IR signature, exactly the
|
||||
// widened discipline.
|
||||
const named = staticNameBindings(lowerer, expr, info);
|
||||
const keyofTps = keyofConstrainedTypeParams(info);
|
||||
if (keyofTps.size > 0) {
|
||||
if (keyofConstrainedTypeParams(info).size > 0) {
|
||||
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
||||
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
||||
const litKey = info.typeParams
|
||||
.filter((tp) => keyofTps.has(tp))
|
||||
.map((tp) => {
|
||||
const bound = tsBindings.get(tp);
|
||||
return bound?.isStringLiteralType()
|
||||
? JSON.stringify(bound.value)
|
||||
: bound?.isNumberLiteralType()
|
||||
? String(bound.value)
|
||||
: "*";
|
||||
})
|
||||
.join(",");
|
||||
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
|
||||
extraKey: `@${litKey}${named?.key ?? ""}`,
|
||||
extraKey: `${keyofLiteralKey(info, tsBindings)}${named?.key ?? ""}`,
|
||||
tsBindings,
|
||||
...(named ? { staticFnNames: named.names } : {}),
|
||||
});
|
||||
@@ -363,6 +347,110 @@ export function genericCallInstance(
|
||||
);
|
||||
}
|
||||
|
||||
/** A call that resolved to an OVERLOAD signature of a generic function
|
||||
* or method with an implementation (`request<T>(p: WithHandler<T>):
|
||||
* Promise<T>; request(p: Plain): Promise<Res>; request<T>(p: Params<T>)
|
||||
* {...}`): the implementation monomorphizes under the bindings the
|
||||
* overload's resolved signature proves for its type parameters (the
|
||||
* structural unification every generic call uses, run against the
|
||||
* overload; unbound parameters take their declared defaults). The
|
||||
* instance's parameters and result are the IMPLEMENTATION's under those
|
||||
* bindings — the body was checked against them — and the call's
|
||||
* arguments and result coerce across, exactly as a non-generic overload
|
||||
* call reconciles with its implementation (parameters the overload omits
|
||||
* must be optional in the implementation). Null when the overload does
|
||||
* not belong to this implementation or the arities do not line up. */
|
||||
function overloadCallInstance(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
info: GenericFnInfo,
|
||||
rsig: ts.Signature,
|
||||
rdecl: ts.FunctionDeclaration | ts.MethodDeclaration,
|
||||
): GenericInstance | null {
|
||||
const impl = info.decl;
|
||||
if (
|
||||
rdecl.parent !== impl.parent ||
|
||||
rdecl.name === undefined ||
|
||||
impl.name === undefined ||
|
||||
rdecl.name.getText() !== impl.name.getText() ||
|
||||
rdecl.parameters.nodes.length > impl.parameters.nodes.length ||
|
||||
impl.parameters.nodes
|
||||
.slice(rdecl.parameters.nodes.length)
|
||||
.some((p) => p.questionToken === undefined && p.initializer === undefined) ||
|
||||
rdecl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined)
|
||||
)
|
||||
return null;
|
||||
const declSig = lowerer.checker.getSignatureFromDeclaration(impl);
|
||||
if (!declSig) return null;
|
||||
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
||||
const bindings = new Map<ts.Symbol, IrType>();
|
||||
unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings);
|
||||
// Explicit type arguments name the OVERLOAD's type parameters, which
|
||||
// need not match the implementation's in order: the resolved overload
|
||||
// already carries them, so unification binds first, and an explicit
|
||||
// argument binds only a still-unbound implementation parameter of the
|
||||
// same name.
|
||||
const overloadTps = rdecl.typeParameters?.nodes ?? [];
|
||||
expr.typeArguments?.nodes.forEach((ta, i) => {
|
||||
const name = overloadTps[i]?.name.text;
|
||||
const tp = info.typeParams.find((p) => ts.symbolName(p) === name);
|
||||
if (!tp || bindings.has(tp)) return;
|
||||
const taT = lowerer.checker.getTypeFromTypeNode(ta);
|
||||
const mapped = lowerer.mapTypeOf(taT);
|
||||
if (mapped) {
|
||||
bindings.set(tp, mapped);
|
||||
tsBindings.set(tp, taT);
|
||||
}
|
||||
});
|
||||
bindDefaultTypeParams(lowerer, info.typeParams, impl.typeParameters?.nodes, bindings, tsBindings);
|
||||
// A type parameter the overload never mentions (a handler the overload
|
||||
// omits) is `unknown`, the checker's own answer for an uninferred one.
|
||||
for (const tp of info.typeParams) {
|
||||
if (bindings.has(tp)) continue;
|
||||
bindings.set(tp, DYN);
|
||||
tsBindings.set(tp, lowerer.checker.getUnknownType());
|
||||
}
|
||||
const previous = lowerer.typeParamBindings;
|
||||
const previousTs = lowerer.typeParamTsBindings;
|
||||
lowerer.typeParamBindings = bindings;
|
||||
lowerer.typeParamTsBindings = tsBindings;
|
||||
let params: ParamShape[];
|
||||
let returnType: IrType | null;
|
||||
try {
|
||||
params = paramShapes(
|
||||
lowerer,
|
||||
impl.parameters.nodes,
|
||||
declSig,
|
||||
(_param, i) => expr.arguments.nodes[i] ?? expr,
|
||||
);
|
||||
returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig));
|
||||
} finally {
|
||||
lowerer.typeParamBindings = previous;
|
||||
lowerer.typeParamTsBindings = previousTs;
|
||||
}
|
||||
if (!returnType) return null;
|
||||
// keyof-constrained parameters key their instance on the bound literal,
|
||||
// as on the implementation-signature path.
|
||||
const runtimeReturn = runtimeReturnType(lowerer, info, returnType);
|
||||
return internGenericInstance(lowerer, expr, info, params, runtimeReturn, () => bindings, {
|
||||
...(keyofConstrainedTypeParams(info).size > 0
|
||||
? { extraKey: keyofLiteralKey(info, tsBindings) }
|
||||
: {}),
|
||||
tsBindings,
|
||||
});
|
||||
}
|
||||
|
||||
/** An instance's result type: a body that can return an indexed read
|
||||
* that misses returns undefined there, as in JavaScript, whatever the
|
||||
* declared result says. An async body's promise is always present and
|
||||
* settles with undefined there. */
|
||||
function runtimeReturnType(lowerer: Lowerer, info: GenericFnInfo, mapped: IrType): IrType {
|
||||
if (!lowerer.runtimeOptionalGenericReturns.has(info.decl)) return mapped;
|
||||
return mapped.kind === "promise"
|
||||
? { kind: "promise", inner: lowerer.runtimeOptionalType(mapped.inner) }
|
||||
: lowerer.runtimeOptionalType(mapped);
|
||||
}
|
||||
|
||||
/** A generic factory whose result is one of its own local classes
|
||||
* (`function base<T>(v: T) { return class { value: T = v }; }`). Each
|
||||
* instance has its own class, so the instance is keyed on its bindings and
|
||||
@@ -596,6 +684,25 @@ function keyofConstrainedTypeParams(info: GenericFnInfo): Set<ts.Symbol> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The instance-key suffix of the literals bound to `info`'s
|
||||
* keyof-constrained type parameters (`@"a"`; `*` for a non-literal
|
||||
* binding): two literals can map to one IR signature while their bodies
|
||||
* read different fields. */
|
||||
function keyofLiteralKey(info: GenericFnInfo, tsBindings: Map<ts.Symbol, ts.Type>): string {
|
||||
const keyofTps = keyofConstrainedTypeParams(info);
|
||||
const literals = info.typeParams
|
||||
.filter((tp) => keyofTps.has(tp))
|
||||
.map((tp) => {
|
||||
const bound = tsBindings.get(tp);
|
||||
return bound?.isStringLiteralType()
|
||||
? JSON.stringify(bound.value)
|
||||
: bound?.isNumberLiteralType()
|
||||
? String(bound.value)
|
||||
: "*";
|
||||
});
|
||||
return `@${literals.join(",")}`;
|
||||
}
|
||||
|
||||
/** Type-parameter symbol → concrete IR type for one instantiation.
|
||||
* Explicit type arguments bind directly; the rest come from structurally
|
||||
* matching each DECLARED param/return type (which mentions the type
|
||||
@@ -736,6 +843,27 @@ function unifySignatureBindings(
|
||||
// so match it against the whole remaining union. Removing null
|
||||
// too is safe only when no undeclared null arm remains.
|
||||
unify(dParts[0]!, lowerer.checker.getNonNullableType(inst), depth + 1);
|
||||
} else if (dParts.length > 1 && iParts.length === dParts.length) {
|
||||
// `T | Promise<T>` against `Res | Promise<Res>`: each declared
|
||||
// part that names a generic target (Promise, an interface, an
|
||||
// alias) pairs with the one instantiated part of the same target;
|
||||
// a single remaining bare type parameter takes the single
|
||||
// remaining instantiated part.
|
||||
const remaining = [...iParts];
|
||||
const bare: ts.Type[] = [];
|
||||
for (const d of dParts) {
|
||||
if (d.flags & ts.TypeFlags.TypeParameter) {
|
||||
bare.push(d);
|
||||
continue;
|
||||
}
|
||||
const target = d.getSymbol() ?? d.getAliasSymbol();
|
||||
if (target === undefined) return;
|
||||
const matches = remaining.filter((i) => (i.getSymbol() ?? i.getAliasSymbol()) === target);
|
||||
if (matches.length !== 1) return;
|
||||
remaining.splice(remaining.indexOf(matches[0]!), 1);
|
||||
unify(d, matches[0]!, depth + 1);
|
||||
}
|
||||
if (bare.length === 1 && remaining.length === 1) unify(bare[0]!, remaining[0]!, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3367,6 +3367,8 @@ function lowerDynReceiverMethodCall(
|
||||
? lowerer.checker.getNonNullableType(rawRecvTs)
|
||||
: rawRecvTs;
|
||||
if (lowerer.classImplementedProtocol(recvTs)) {
|
||||
const viewed = pickViewGenericMethodCall(lowerer, call, access, recvTs);
|
||||
if (viewed) return viewed;
|
||||
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
|
||||
if (
|
||||
property &&
|
||||
@@ -10411,6 +10413,53 @@ function maybePromiseHandlerResult(
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/** `view.m(args)` where `view` is a `Pick<C, K>` / `Omit<C, K>` view of a
|
||||
* program class C and `m` is one of C's GENERIC methods (its overloads
|
||||
* included): generic methods have no dispatchable method value, so the
|
||||
* view's live receiver is extracted as the C instance it holds (a checked
|
||||
* extraction — a value of another class throws Node-style TypeError
|
||||
* text naming C) and the call monomorphizes statically against C, the
|
||||
* same lowering a C-typed receiver gets. Null for other receivers. */
|
||||
function pickViewGenericMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
recvTs: ts.Type,
|
||||
): IrExpr | null {
|
||||
const alias = recvTs.getAliasSymbol();
|
||||
if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit"))
|
||||
return null;
|
||||
if (!alias.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return null;
|
||||
const source = recvTs.getAliasTypeArguments()[0];
|
||||
const symbol = source?.getSymbol();
|
||||
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
||||
if (!info || call.questionDotToken || access.questionDotToken) return null;
|
||||
const found = findGenericMethodOn(lowerer, info, access.name.text);
|
||||
if (!found) return null;
|
||||
const loc = locOf(access.expression);
|
||||
const lowered = lowerer.lowerExpr(access.expression);
|
||||
const instance: IrType = { kind: "object", className: info.def.name };
|
||||
// A view that statically holds an instance of another class (a
|
||||
// structurally compatible one) calls that class's method in Node, not
|
||||
// C's: the call is refused rather than extracted as a C instance.
|
||||
if (
|
||||
lowered.type.kind === "object" &&
|
||||
lowered.type.className !== info.def.name &&
|
||||
!lowerer.isSubclassOf(lowered.type.className, info.def.name)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
`calling the generic method '${access.name.text}' through a '${ts.symbolName(alias)}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`,
|
||||
);
|
||||
}
|
||||
const receiver: IrExpr =
|
||||
lowered.type.kind === "object"
|
||||
? lowered
|
||||
: { kind: "dynCheck", value: lowerer.coerceToExpected(lowered, DYN), type: instance, loc };
|
||||
return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver);
|
||||
}
|
||||
|
||||
/** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy
|
||||
* target (`Object.assign(new Error(message), details)`): the sources'
|
||||
* own members land on the error — the runtime copy over its
|
||||
|
||||
@@ -2138,7 +2138,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
(ts.isInterfaceDeclaration(d) ||
|
||||
ts.isTypeAliasDeclaration(d) ||
|
||||
ts.isTypeLiteralNode(d) ||
|
||||
ts.isMappedTypeNode(d)),
|
||||
ts.isMappedTypeNode(d) ||
|
||||
isDeclaredDataClass(d) ||
|
||||
(decls.some(isDeclaredDataClass) && isTypeOnlyNamespace(d))),
|
||||
);
|
||||
const aliasDecls = packageOnly(widened.getAliasSymbol());
|
||||
const ownDecls = packageOnly(widened.getSymbol());
|
||||
@@ -6520,6 +6522,48 @@ function isMappedShape(t: ts.Type): boolean {
|
||||
* of a package the program does not compile. Its pure type shapes —
|
||||
* interfaces, aliases, type literals — describe data a program can build
|
||||
* itself (`import type` from a types-only package such as type-fest). */
|
||||
/** A class an SDK declares only to describe data (aws-sdk's
|
||||
* `declare class PresignedPost { url: string; fields: Fields }`, which
|
||||
* arrives as JSON and is never constructed): public instance fields and
|
||||
* nothing else — no constructor, methods, accessors, index signatures,
|
||||
* static, private or protected members, base classes, implemented
|
||||
* interfaces or type parameters. Its instances are exactly the records
|
||||
* of those fields, so the type maps structurally like an interface. */
|
||||
function isDeclaredDataClass(d: ts.Node): boolean {
|
||||
if (!ts.isClassDeclaration(d) || !d.getSourceFile().isDeclarationFile) return false;
|
||||
if (d.heritageClauses !== undefined && d.heritageClauses.nodes.length > 0) return false;
|
||||
if (d.typeParameters !== undefined && d.typeParameters.nodes.length > 0) return false;
|
||||
return d.members.nodes.every(
|
||||
(member) =>
|
||||
ts.isPropertyDeclaration(member) &&
|
||||
!ts.isPrivateIdentifier(member.name) &&
|
||||
!(member.modifiers?.nodes ?? []).some(
|
||||
(modifier) =>
|
||||
modifier.kind === ts.SyntaxKind.StaticKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.PrivateKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.ProtectedKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.AbstractKeyword,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** A namespace merged into a declared data class that holds only types
|
||||
* (`namespace PresignedPost { interface Fields { … } }`): it adds no value
|
||||
* the program could reach. */
|
||||
function isTypeOnlyNamespace(d: ts.Node): boolean {
|
||||
if (!ts.isModuleDeclaration(d)) return false;
|
||||
const body = d.body;
|
||||
if (body === undefined) return true;
|
||||
if (ts.isModuleDeclaration(body)) return isTypeOnlyNamespace(body);
|
||||
if (!ts.isModuleBlock(body)) return false;
|
||||
return body.statements.nodes.every(
|
||||
(statement) =>
|
||||
ts.isInterfaceDeclaration(statement) ||
|
||||
ts.isTypeAliasDeclaration(statement) ||
|
||||
isTypeOnlyNamespace(statement),
|
||||
);
|
||||
}
|
||||
|
||||
function packageTypeFile(sf: ts.SourceFile, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
|
||||
return (
|
||||
sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf)
|
||||
@@ -7287,10 +7331,11 @@ function mapRecordTypeInner(
|
||||
let v: IrType | null = null;
|
||||
for (const info of indexInfos) {
|
||||
if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null;
|
||||
const iv = mapType(info.type, ctx);
|
||||
|
||||
// JSONValue>` (a package's own JSON alias) and `Record<string, any>`
|
||||
|
||||
// An `any`-valued signature (`{ [key: string]: any }`, an SDK's
|
||||
// `Dictionary<any>`) holds whatever the program stores: its
|
||||
// overflow values are checked-dynamic, exactly `Record<string,
|
||||
// unknown>`'s.
|
||||
const iv = info.type.flags & ts.TypeFlags.Any ? DYN : mapType(info.type, ctx);
|
||||
if (!iv || !isSupportedIndexValue(iv)) return null;
|
||||
if (v !== null && !typeEquals(v, iv)) return null;
|
||||
v = iv;
|
||||
@@ -7517,6 +7562,22 @@ function mapRecordTypeInner(
|
||||
pt = arrayOf(unitOnlyUnion(ctx.unions));
|
||||
}
|
||||
if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN;
|
||||
// An `any[]` field (`list: any[]` in an SDK's message type, also
|
||||
// optional or nullable) is the checked-dynamic array, exactly as
|
||||
// `unknown[]` maps; the checked-dynamic value holds the units too.
|
||||
// Declared members only: an object literal's inferred `any[]` member
|
||||
// keeps its existing story.
|
||||
if (
|
||||
pt === null &&
|
||||
declaredMember(p) &&
|
||||
(fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every(
|
||||
(part) =>
|
||||
isAnyArrayType(part, checker) ||
|
||||
(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0,
|
||||
) &&
|
||||
!isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)
|
||||
)
|
||||
pt = DYN;
|
||||
// Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union
|
||||
// absent-field idiom — and `{ p: undefined }` spellings): the
|
||||
// unit-only union. The runtime value is the interned unit, JSON
|
||||
@@ -7613,6 +7674,10 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
return true;
|
||||
case "record":
|
||||
case "array":
|
||||
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
|
||||
// holds a promise by reference, so `unknown` absorbs the promise arm
|
||||
// exactly as the checker's own union reduction does.
|
||||
case "promise":
|
||||
return canConvertToDyn(
|
||||
arm,
|
||||
(id) => ctx.shapes.get(id),
|
||||
@@ -7907,15 +7972,118 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
|
||||
if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue;
|
||||
return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`;
|
||||
}
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
if (pt === undefined) continue;
|
||||
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam))
|
||||
pt = unitOnlyUnion(ctx.unions);
|
||||
if (!pt || pt.kind === "void") {
|
||||
return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`;
|
||||
if (memberMaps(fieldTs, ctx, p)) continue;
|
||||
// Name the deepest member the failure comes from (`order.deployment.
|
||||
// builds[].config`): a large message type otherwise points only at
|
||||
// its outermost member.
|
||||
const inner = innerBlockedMember(fieldTs, ctx, new Set());
|
||||
if (inner !== null && inner.path !== "") {
|
||||
return `the record shape is supported, but its member '${ts.symbolName(p)}${inner.path}' has type '${checker.typeToString(inner.type)}', which does not compile`;
|
||||
}
|
||||
return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A member an interface, type literal or class declares (not one an
|
||||
* object literal's inferred type carries). */
|
||||
function declaredMember(member: ts.Symbol): boolean {
|
||||
return (
|
||||
member.declarations.length > 0 &&
|
||||
member.declarations.every((d) => ts.isPropertySignature(d) || ts.isPropertyDeclaration(d))
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a record member of type `fieldTs` maps, by the member rules of
|
||||
* mapRecordTypeInner (generic members erase; `any` and `any[]` members
|
||||
* are checked-dynamic; unit-only members are the unit union). Overloaded
|
||||
* members answer true: describeOverloadSetBlocker tells their story. */
|
||||
function memberMaps(fieldTs: ts.Type, ctx: TypeMapperCtx, member?: ts.Symbol): boolean {
|
||||
const { checker } = ctx;
|
||||
if (isOverloadedCallableMemberType(fieldTs, checker)) return true;
|
||||
const pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
if (pt === undefined) return true;
|
||||
if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) return true;
|
||||
if (
|
||||
pt === null &&
|
||||
(member === undefined || declaredMember(member)) &&
|
||||
(fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every(
|
||||
(part) =>
|
||||
isAnyArrayType(part, checker) ||
|
||||
(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0,
|
||||
) &&
|
||||
!isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)
|
||||
)
|
||||
return true;
|
||||
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) return true;
|
||||
return pt !== null && pt.kind !== "void";
|
||||
}
|
||||
|
||||
/** The member path inside the unmapped type `t` that its failure comes
|
||||
* from, and that member's type: `.name` steps into record members, `[]`
|
||||
* into array elements, `()` into a function's return; union arms and
|
||||
* promise payloads are transparent. The empty path means `t` itself is
|
||||
* the blocker; null means `t` maps (or is `any`). */
|
||||
function innerBlockedMember(
|
||||
t: ts.Type,
|
||||
ctx: TypeMapperCtx,
|
||||
seen: Set<ts.Type>,
|
||||
): { path: string; type: ts.Type } | null {
|
||||
const { checker } = ctx;
|
||||
// `any` is no component to name: the member holding it is the blocker.
|
||||
if ((t.flags & ts.TypeFlags.Any) !== 0 || mapType(t, ctx) !== null) return null;
|
||||
const here = { path: "", type: t };
|
||||
if (seen.has(t) || seen.size >= 16) return here;
|
||||
seen.add(t);
|
||||
const step = (prefix: string, inner: ts.Type): { path: string; type: ts.Type } => {
|
||||
const found = innerBlockedMember(inner, ctx, seen);
|
||||
return found === null ? here : { path: prefix + found.path, type: found.type };
|
||||
};
|
||||
if (t.isUnionType()) {
|
||||
const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null;
|
||||
for (const part of ts.constituentTypes(t)) {
|
||||
if ((part.flags & UNIT) !== 0 || mapType(part, ctx) !== null) continue;
|
||||
return step("", part);
|
||||
}
|
||||
return here;
|
||||
}
|
||||
if ((t.flags & ts.TypeFlags.Object) === 0) return here;
|
||||
if (checker.isArrayType(t)) {
|
||||
const elem = checker.getTypeArguments(t as ts.TypeReference)[0];
|
||||
return elem === undefined ? here : step("[]", elem);
|
||||
}
|
||||
const symbol = t.getSymbol();
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "Promise" &&
|
||||
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
|
||||
) {
|
||||
const payload = checker.getTypeArguments(t as ts.TypeReference)[0];
|
||||
return payload === undefined ? here : step("", payload);
|
||||
}
|
||||
const calls = checker.getCallSignatures(t);
|
||||
if (calls.length === 1) {
|
||||
const sig = calls[0]!;
|
||||
if (sig.getTypeParameters().length > 0) return here;
|
||||
if (sig.getParameters().some((p) => mapType(checker.getTypeOfSymbol(p), ctx) === null))
|
||||
return here;
|
||||
return step("()", checker.getReturnTypeOfSignature(sig));
|
||||
}
|
||||
if (
|
||||
calls.length > 0 ||
|
||||
checker.getConstructSignatures(t).length > 0 ||
|
||||
checker.isTupleType(t) ||
|
||||
checker.getIndexInfosOfType(t).length > 0
|
||||
)
|
||||
return here;
|
||||
for (const p of checker.getPropertiesOfType(t)) {
|
||||
if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue;
|
||||
if (ts.symbolName(p).startsWith("__@")) continue;
|
||||
const fieldTs = checker.getTypeOfSymbol(p);
|
||||
if (memberMaps(fieldTs, ctx, p)) continue;
|
||||
return step(`.${ts.symbolName(p)}`, fieldTs);
|
||||
}
|
||||
return here;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6412-any-valued-dictionaries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6413-sdk-declared-data-class/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6414-pick-class-method-views.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6495-generic-method-overloads.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6496-overload-signatures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6497-generic-overload-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/nested-member-blocker-path.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/overload-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/pick-view-generic-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Message types whose members are typed with `any`: string-keyed
|
||||
// dictionaries of `any` values (`{ [key: string]: any }`, a `Dictionary<T =
|
||||
// any>` alias used bare), and `any[]` lists, also optional, nested in
|
||||
// arrays of records. Values keep whatever the program or JSON stored.
|
||||
type Dictionary<T = any> = { [key: string]: T };
|
||||
|
||||
interface Build {
|
||||
use: string;
|
||||
src?: string;
|
||||
config?: Dictionary;
|
||||
}
|
||||
interface Order {
|
||||
id: string;
|
||||
builds?: Build[];
|
||||
upload: { url: string; fields: { [key: string]: any } };
|
||||
tags: any[];
|
||||
extra?: any[];
|
||||
}
|
||||
|
||||
function tags(): Dictionary<any> {
|
||||
return { a: 1, b: "x", c: [1, 2] };
|
||||
}
|
||||
async function meta(): Promise<Dictionary> {
|
||||
return { n: 3 };
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const order: Order = JSON.parse(
|
||||
'{"id":"o1","builds":[{"use":"node","config":{"zero":true,"n":2}},{"use":"static","src":"a/**"}],' +
|
||||
'"upload":{"url":"u","fields":{"k":"v","n":2}},"tags":[1,"two",null]}',
|
||||
);
|
||||
console.log(order.id, order.upload.url, String(order.upload.fields.k), order.upload.fields["n"] + 1);
|
||||
for (const build of order.builds ?? []) {
|
||||
console.log(build.use, build.src ?? "-", JSON.stringify(build.config ?? null));
|
||||
}
|
||||
console.log(order.tags.length, order.extra === undefined, JSON.stringify(order.tags));
|
||||
|
||||
const t = tags();
|
||||
for (const k of Object.keys(t)) console.log(k, JSON.stringify(t[k]));
|
||||
t.d = { deep: true };
|
||||
console.log(JSON.stringify(t));
|
||||
const m = await meta();
|
||||
console.log(m.n * 2, JSON.stringify(m));
|
||||
|
||||
const made: Order = { id: "o2", upload: { url: "w", fields: {} }, tags: [], extra: [0] };
|
||||
made.upload.fields.signature = "s";
|
||||
made.tags.push({ k: 1 });
|
||||
console.log(JSON.stringify(made));
|
||||
}
|
||||
void main();
|
||||
@@ -0,0 +1,27 @@
|
||||
// A types-only SDK package declares the shape of JSON data as a class with
|
||||
// fields only (`declare class UploadForm { url: string; fields: ... }`,
|
||||
// with a merged namespace holding the fields interface). The program never
|
||||
// constructs it: values arrive through JSON.parse or object literals, so
|
||||
// they compile as records of the declared fields.
|
||||
import type { UploadForm } from "upload-sdk";
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
upload?: UploadForm;
|
||||
forms: UploadForm[];
|
||||
}
|
||||
|
||||
function describe(form: UploadForm | undefined): string {
|
||||
if (!form) return "none";
|
||||
return `${form.url} ${form.fields.Policy} ${form.fields["X-Signature"]} ${form.fields["key"] ?? "-"} ${Object.keys(form.fields).join(",")}`;
|
||||
}
|
||||
|
||||
const order = JSON.parse(
|
||||
'{"id":"o1","upload":{"url":"u","fields":{"Policy":"p","X-Signature":"s","key":"k"}},"forms":[]}',
|
||||
) as Order;
|
||||
console.log(order.id, describe(order.upload));
|
||||
const made: UploadForm = { url: "v", fields: { Policy: "q", "X-Signature": "t" } };
|
||||
order.forms.push(made);
|
||||
made.fields.extra = "e";
|
||||
console.log(describe(made), JSON.stringify(order.forms));
|
||||
console.log(describe(undefined));
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export declare class UploadForm {
|
||||
/** The URL the form posts to. */
|
||||
url: string;
|
||||
/** The hidden form fields. */
|
||||
fields: UploadForm.Fields;
|
||||
}
|
||||
export declare namespace UploadForm {
|
||||
interface Fields {
|
||||
Policy: string;
|
||||
"X-Signature": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "upload-sdk", "version": "1.0.0", "types": "index.d.ts" }
|
||||
@@ -0,0 +1,83 @@
|
||||
// `Pick<C, K>` and `Omit<C, K>` over a class, used as structural parameter
|
||||
// and field types: a view of the instance's own members. Calls through the
|
||||
// view run the instance's methods on the instance itself, so its state is
|
||||
// shared with every other reference.
|
||||
class ApiClient {
|
||||
private readonly base: string;
|
||||
calls = 0;
|
||||
constructor(base: string) {
|
||||
this.base = base;
|
||||
}
|
||||
request(path: string): Promise<string> {
|
||||
this.calls++;
|
||||
return Promise.resolve(this.base + path);
|
||||
}
|
||||
close(): void {
|
||||
console.log("closed after", this.calls);
|
||||
}
|
||||
}
|
||||
|
||||
interface Context {
|
||||
client: Pick<ApiClient, "request">;
|
||||
name: string;
|
||||
}
|
||||
|
||||
async function run(ctx: Context): Promise<void> {
|
||||
console.log(ctx.name, await ctx.client.request("/x"), await ctx.client.request("/y"));
|
||||
}
|
||||
function finish(client: Pick<ApiClient, "request" | "close">): void {
|
||||
client.close();
|
||||
}
|
||||
function count(client: Omit<ApiClient, "close">): number {
|
||||
return client.calls;
|
||||
}
|
||||
|
||||
const client = new ApiClient("https://h");
|
||||
void run({ client, name: "ctx" }).then(() => {
|
||||
finish(client);
|
||||
console.log(count(client), client.calls);
|
||||
});
|
||||
|
||||
// A live view that flows into another structural type keeps the instance:
|
||||
// reads through the second view see the instance's later writes.
|
||||
interface Counter {
|
||||
calls: number;
|
||||
}
|
||||
function counted(view: Pick<ApiClient, "request" | "calls">): Counter {
|
||||
void view.request("/z");
|
||||
return view;
|
||||
}
|
||||
const second = new ApiClient("https://k");
|
||||
const counter = counted(second);
|
||||
second.calls += 10;
|
||||
const holder: { counter: Counter } = { counter: counted(second) };
|
||||
second.calls = 100;
|
||||
console.log("counter", counter.calls, holder.counter.calls, second.calls);
|
||||
|
||||
// A view also accepts other classes that have its members. Their instances
|
||||
// flow through it the same way.
|
||||
class Meter {
|
||||
label = "meter";
|
||||
reads = 0;
|
||||
read(): number {
|
||||
return this.reads;
|
||||
}
|
||||
}
|
||||
class Gauge {
|
||||
reads = 0;
|
||||
read(): number {
|
||||
return this.reads;
|
||||
}
|
||||
}
|
||||
interface Reading {
|
||||
reads: number;
|
||||
}
|
||||
function reading(view: Pick<Meter, "read" | "reads">): Reading {
|
||||
console.log("read", view.read());
|
||||
return view;
|
||||
}
|
||||
const gauge = new Gauge();
|
||||
const result = reading(gauge);
|
||||
gauge.reads = 50;
|
||||
const meter = new Meter();
|
||||
console.log("reading", result.reads, reading(meter).reads, meter.label);
|
||||
@@ -0,0 +1,54 @@
|
||||
// Calls that resolve to OVERLOAD signatures of a generic method or
|
||||
// function: the implementation monomorphizes under what the overload
|
||||
// proves for its type parameters (a parameter the overload never mentions
|
||||
// is unknown), directly and through a `Pick` view of the class, whose
|
||||
// live receiver keeps the instance's state. `T | Promise<T>` callbacks
|
||||
// bind T from either arm.
|
||||
interface Reply {
|
||||
code: number;
|
||||
text: string;
|
||||
}
|
||||
type Mapper<T> = (reply: Reply) => T | Promise<T>;
|
||||
|
||||
class Store {
|
||||
loads = 0;
|
||||
async load<T>(key: string, map: Mapper<T>): Promise<T>;
|
||||
async load(key: string): Promise<Reply>;
|
||||
async load<T>(key: string, map?: Mapper<T>): Promise<T | Reply> {
|
||||
this.loads++;
|
||||
const reply: Reply = { code: 200, text: key };
|
||||
if (map === undefined) return reply;
|
||||
return await map(reply);
|
||||
}
|
||||
}
|
||||
|
||||
interface Services {
|
||||
store: Pick<Store, "load">;
|
||||
}
|
||||
|
||||
function pick<T>(items: T[], index: number): T;
|
||||
function pick<T>(items: T[]): T;
|
||||
function pick<T>(items: T[], index = 0): T {
|
||||
return items[index]!;
|
||||
}
|
||||
|
||||
async function settle<T>(reply: Reply, map?: Mapper<T>): Promise<T | Reply> {
|
||||
return map ? await map(reply) : reply;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const store = new Store();
|
||||
const plain = await store.load("alpha");
|
||||
console.log(plain.code, plain.text);
|
||||
const length = await store.load("beta", (reply) => reply.text.length);
|
||||
console.log(length * 2);
|
||||
const services: Services = { store };
|
||||
const viaView = await services.store.load("gamma");
|
||||
const upper = await services.store.load("delta", async (reply) => reply.text.toUpperCase());
|
||||
console.log(viaView.text, upper, store.loads);
|
||||
console.log(pick(["x", "y"]), pick([1, 2, 3], 2));
|
||||
console.log(JSON.stringify(await settle<Reply>({ code: 1, text: "s" })));
|
||||
console.log(await settle({ code: 2, text: "tt" }, (reply) => reply.code + reply.text.length));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
// The overload fences that REMAIN now that overload signatures lower:
|
||||
// generic overload calls (the per-instantiation body would be typed
|
||||
// against a signature it was never checked under), and the multi-
|
||||
// signature type surfacing where a binding needs it spelled.
|
||||
// Overload signatures: a generic overload call monomorphizes the
|
||||
// implementation under the bindings the overload proves (a parameter the
|
||||
// overload omits is optional in the implementation), and an immutable
|
||||
// alias of an overloaded declaration projects onto the implementation.
|
||||
function twice<T>(x: T): T[];
|
||||
function twice<T>(x: T, n: number): T[];
|
||||
function twice<T>(x: T, n?: number): T[] {
|
||||
@@ -0,0 +1,47 @@
|
||||
// Bindings of generic overload calls: explicit type arguments name the
|
||||
// overload's type parameters, which can be declared in another order than
|
||||
// the implementation's, and keyof-constrained parameters bound to
|
||||
// different literals through an overload read their own fields.
|
||||
function tag<B, A>(value: A, label: B): string;
|
||||
function tag<A, B>(value: A, label?: B): string {
|
||||
return `${typeof value}:${typeof label}:${String(value)}${String(label)}`;
|
||||
}
|
||||
console.log(tag<string, number>(5, "x"));
|
||||
console.log(tag<boolean, string>("y", true));
|
||||
|
||||
class Labels {
|
||||
prefix = "#";
|
||||
render<B, A>(value: A, label: B): string;
|
||||
render<A, B>(value: A, label?: B): string {
|
||||
return `${this.prefix}${typeof value}/${typeof label}`;
|
||||
}
|
||||
}
|
||||
console.log(new Labels().render<number, string>("z", 7));
|
||||
|
||||
interface Row {
|
||||
a: number;
|
||||
b: number;
|
||||
name: string;
|
||||
}
|
||||
function read<T, K extends keyof T>(row: T, key: K): T[K];
|
||||
function read<T, K extends keyof T>(row: T, key: K, fallback: T[K]): T[K];
|
||||
function read<T, K extends keyof T>(row: T, key: K, fallback?: T[K]): T[K] {
|
||||
const value = row[key];
|
||||
return value === undefined ? fallback! : value;
|
||||
}
|
||||
const row: Row = { a: 1, b: 2, name: "r" };
|
||||
console.log(read(row, "a"), read(row, "b"), read(row, "name"));
|
||||
console.log(read(row, "b", 0) + read(row, "a", 0));
|
||||
|
||||
// An overload call whose implementation returns an indexed read that
|
||||
// misses returns undefined, as the implementation's own calls do.
|
||||
function at<T>(items: T[], index: number): T;
|
||||
function at<T>(items: T[], index: number, label: string): T;
|
||||
function at<T>(items: T[], index: number, label?: string): T {
|
||||
if (label !== undefined) console.log(label);
|
||||
return items[index];
|
||||
}
|
||||
console.log(at([1, 2, 3], 1), at([1, 2, 3], 5));
|
||||
console.log(at(["a", "b"], 0, "strings"), at(["a", "b"], 9, "strings"));
|
||||
const missing = at([{ id: 1 }], 3);
|
||||
console.log(missing === undefined, typeof missing);
|
||||
@@ -0,0 +1,19 @@
|
||||
// A record blocked by a member deep inside it names the member's path:
|
||||
// through a function's result (`()`), the promise and union around it,
|
||||
// record members (`.name`) and array elements (`[]`).
|
||||
interface Job {
|
||||
id: string;
|
||||
handle: Promise<number> & { cancel(): void };
|
||||
}
|
||||
interface Order {
|
||||
id: string;
|
||||
jobs: Job[];
|
||||
}
|
||||
interface Provider {
|
||||
name: string;
|
||||
poll: () => Promise<Order | undefined>;
|
||||
}
|
||||
function makeProvider(): Provider {
|
||||
return { name: "p", poll: async () => undefined };
|
||||
}
|
||||
console.log(makeProvider().name);
|
||||
@@ -0,0 +1,16 @@
|
||||
// A generic method called through a `Pick` view of a class compiles
|
||||
// against that class. A view that holds an instance of a structurally
|
||||
// compatible, unrelated class is refused: Node calls that class's own
|
||||
// method.
|
||||
class Store {
|
||||
load<T>(key: string, map: (text: string) => T): T {
|
||||
return map(key);
|
||||
}
|
||||
}
|
||||
class Mirror {
|
||||
load<T>(key: string, map: (text: string) => T): T {
|
||||
return map(`${key}!`);
|
||||
}
|
||||
}
|
||||
const view: Pick<Store, "load"> = new Mirror();
|
||||
console.log(view.load("k", (text) => text.length));
|
||||
@@ -0,0 +1,17 @@
|
||||
nested-member-blocker-path.ts:16:10 - error SC2009: values of type 'Provider' cannot be compiled: the record shape is supported, but its member 'poll().jobs[].handle' has type 'Promise<number> & { cancel(): void; }', which does not compile
|
||||
|
||||
15 | }
|
||||
16 | function makeProvider(): Provider {
|
||||
| ^~~~~~~~~~~~
|
||||
17 | return { name: "p", poll: async () => undefined };
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
nested-member-blocker-path.ts:19:13 - error SC2004: uses of 'makeProvider' inherit the blocker on its declaration
|
||||
|
||||
18 | }
|
||||
19 | console.log(makeProvider().name);
|
||||
| ^~~~~~~~~~~~
|
||||
20 |
|
||||
|
||||
hint: the declaration of 'makeProvider' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -1,6 +0,0 @@
|
||||
overload-fences.ts:12:13 - error SC1090: calls selecting an overload signature of a generic function (only the implementation signature monomorphizes) is not supported yet
|
||||
|
||||
11 | }
|
||||
12 | console.log(twice("q").length);
|
||||
| ^~~~~~~~~~
|
||||
13 | function pick(x: "a"): string;
|
||||
@@ -0,0 +1,6 @@
|
||||
pick-view-generic-methods.ts:16:13 - error SC1090: calling the generic method 'load' through a 'Pick' view of 'Store' that holds a 'Mirror' is not supported yet
|
||||
|
||||
15 | const view: Pick<Store, "load"> = new Mirror();
|
||||
16 | console.log(view.load("k", (text) => text.length));
|
||||
| ^~~~~~~~~
|
||||
17 |
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2783, "san": 2211 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2032, "san": 1924 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2061, "san": 1900 }
|
||||
Reference in New Issue
Block a user